Inital Commit for Moonie :)

This commit is contained in:
Luna 2025-05-12 11:53:27 +02:00
commit 0088a48fee
86 changed files with 26060 additions and 0 deletions

7
.dockerignore Normal file
View file

@ -0,0 +1,7 @@
node_modules
npm-debug.log
dist
.vscode
.git
.gitignore
README.md

10
.gitignore vendored Normal file
View file

@ -0,0 +1,10 @@
.DS_Store
node_modules
/build
/.svelte-kit
/package
.env
.env.*
!.env.example
vite.config.js.timestamp-*
vite.config.ts.timestamp-*

7
.vscode/extensions.json vendored Normal file
View file

@ -0,0 +1,7 @@
{
"recommendations": [
"svelte.svelte-vscode",
"tauri-apps.tauri-vscode",
"rust-lang.rust-analyzer"
]
}

3
.vscode/settings.json vendored Normal file
View file

@ -0,0 +1,3 @@
{
"svelte.enable-ts-plugin": true
}

23
Dockerfile Normal file
View file

@ -0,0 +1,23 @@
# Use an official Node runtime as the base image
FROM node:20
# Set the working directory in the container
WORKDIR /app
# Copy package.json and package-lock.json to the working directory
COPY package*.json ./
# Install project dependencies
RUN npm install
# Copy the rest of the application code
COPY . .
# Build the application
RUN npm run build
# Expose the port the app runs on
EXPOSE 4173
# Command to run the application
CMD ["npm", "run", "preview", "--", "--host", "0.0.0.0"]

7
README.md Normal file
View file

@ -0,0 +1,7 @@
# Tauri + SvelteKit + TypeScript
This template should help get you started developing with Tauri, SvelteKit and TypeScript in Vite.
## Recommended IDE Setup
[VS Code](https://code.visualstudio.com/) + [Svelte](https://marketplace.visualstudio.com/items?itemName=svelte.svelte-vscode) + [Tauri](https://marketplace.visualstudio.com/items?itemName=tauri-apps.tauri-vscode) + [rust-analyzer](https://marketplace.visualstudio.com/items?itemName=rust-lang.rust-analyzer).

7474
package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

44
package.json Normal file
View file

@ -0,0 +1,44 @@
{
"name": "luna-cdn",
"version": "0.1.0",
"description": "",
"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",
"tauri": "tauri"
},
"license": "MIT",
"dependencies": {
"@sveltejs/adapter-node": "^5.2.8",
"@tauri-apps/api": ">=2.0.0",
"@tauri-apps/plugin-shell": ">=2.0.0",
"@types/d3": "^7.4.3",
"d3": "^7.9.0",
"fs": "^0.0.1-security",
"marked": "^14.1.4",
"moons-ta-client": "^1.1.13",
"prismjs": "^1.30.0",
"uuid": "^11.1.0"
},
"devDependencies": {
"@sveltejs/adapter-static": "^3.0.1",
"@sveltejs/kit": "^2.0.0",
"@sveltejs/vite-plugin-svelte": "^3.0.0",
"@tauri-apps/cli": ">=2.0.0",
"@types/node": "^22.7.6",
"autoprefixer": "^10.4.20",
"postcss": "^8.4.49",
"svelte": "^4.2.7",
"svelte-check": "^3.6.0",
"tailwindcss": "^3.4.14",
"tslib": "^2.4.1",
"typescript": "^5.0.0",
"vite": "^5.0.3",
"vite-plugin-markdown": "^2.2.0",
"vite-plugin-raw": "^1.0.3"
}
}

2114
pnpm-lock.yaml Normal file

File diff suppressed because it is too large Load diff

6
postcss.config.js Normal file
View file

@ -0,0 +1,6 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

8
serve-prod.js Normal file
View file

@ -0,0 +1,8 @@
import { preview } from 'vite'
const server = await preview({
preview: {
port: 1470,
host: true
}
})

8
serve-staging.js Normal file
View file

@ -0,0 +1,8 @@
import { preview } from 'vite'
const server = await preview({
preview: {
port: 4175,
host: true
}
})

7
src-tauri/.gitignore vendored Normal file
View file

@ -0,0 +1,7 @@
# Generated by Cargo
# will have compiled files and executables
/target/
# Generated by Tauri
# will have schema files for capabilities auto-completion
/gen/schemas

4319
src-tauri/Cargo.lock generated Normal file

File diff suppressed because it is too large Load diff

25
src-tauri/Cargo.toml Normal file
View file

@ -0,0 +1,25 @@
[package]
name = "beatkhanawebsite"
version = "0.1.0"
description = "A Tauri App"
authors = ["you"]
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[lib]
# The `_lib` suffix may seem redundant but it is necessary
# to make the lib name unique and wouldn't conflict with the bin name.
# This seems to be only an issue on Windows, see https://github.com/rust-lang/cargo/issues/8519
name = "beatkhanawebsite_lib"
crate-type = ["staticlib", "cdylib", "rlib"]
[build-dependencies]
tauri-build = { version = "2.0.0", features = [] }
[dependencies]
tauri = { version = "2.0.0", features = [] }
tauri-plugin-shell = "2.0.0"
serde = { version = "1", features = ["derive"] }
serde_json = "1"

3
src-tauri/build.rs Normal file
View file

@ -0,0 +1,3 @@
fn main() {
tauri_build::build()
}

View file

@ -0,0 +1,10 @@
{
"$schema": "../gen/schemas/desktop-schema.json",
"identifier": "default",
"description": "Capability for the main window",
"windows": ["main"],
"permissions": [
"core:default",
"shell:allow-open"
]
}

BIN
src-tauri/icons/128x128.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

BIN
src-tauri/icons/32x32.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 974 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 903 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

BIN
src-tauri/icons/icon.icns Normal file

Binary file not shown.

BIN
src-tauri/icons/icon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

BIN
src-tauri/icons/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

14
src-tauri/src/lib.rs Normal file
View file

@ -0,0 +1,14 @@
// Learn more about Tauri commands at https://tauri.app/v1/guides/features/command
#[tauri::command]
fn greet(name: &str) -> String {
format!("Hello, {}! You've been greeted from Rust!", name)
}
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
.plugin(tauri_plugin_shell::init())
.invoke_handler(tauri::generate_handler![greet])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}

6
src-tauri/src/main.rs Normal file
View file

@ -0,0 +1,6 @@
// Prevents additional console window on Windows in release, DO NOT REMOVE!!
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
fn main() {
beatkhanawebsite_lib::run()
}

36
src-tauri/tauri.conf.json Normal file
View file

@ -0,0 +1,36 @@
{
"$schema": "https://schema.tauri.app/config/2.0.0",
"productName": "beatkhanawebsite",
"version": "0.1.0",
"identifier": "bk.serverbp.dev",
"build": {
"beforeDevCommand": "npm run dev",
"devUrl": "http://localhost:1420",
"beforeBuildCommand": "npm run build",
"frontendDist": "../build"
},
"app": {
"windows": [
{
"title": "beatkhanawebsite",
"width": 800,
"height": 600
}
],
"security": {
"csp": null
}
},
"bundle": {
"active": true,
"targets": "all",
"icon": [
"icons/32x32.png",
"icons/128x128.png",
"icons/128x128@2x.png",
"icons/icon.icns",
"icons/icon.ico"
]
}
}

33
src/app.html Normal file
View file

@ -0,0 +1,33 @@
<!doctype html>
<html lang="en">
<head>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.6.0/css/all.min.css" integrity="sha512-Kc323vGBEqzTmouAECnVceyQqyqdsSiqLQISBL29aUW4U/M7pSPA/gEUZQqv1cwx4OnYxTxve5UMg5GT6L4JJg==" crossorigin="anonymous" referrerpolicy="no-referrer" />
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200" />
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Allerta&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
<meta charset="utf-8" />
<link rel="icon" href="%sveltekit.assets%/talogo.png" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>ShyyTAUI</title>
%sveltekit.head%
</head>
<body data-sveltekit-preload-data="hover">
<div style="display: contents">%sveltekit.body%</div>
</body>
<style>
/* CSS Reset */
*, *::before, *::after {
box-sizing: border-box;
}
html, body {
margin: 0;
padding: 0;
width: 100%;
height: 100%;
overflow-x: hidden;
}
</style>
</html>

View file

@ -0,0 +1,132 @@
<script lang="ts">
export let currentPage: "matches" | "settings" | "qualifiers" | "teams" | "mappools" = "matches";
export let tournamentName;
export let tournamnentGuid;
</script>
<div class="side-menu">
<div class="menu-items">
<a href="/tournaments/{tournamnentGuid}" class="menu-item {currentPage === 'matches' ? 'active' : ''}">
<span class="menu-icon material-icons">sports_esports</span>
<span class="menu-text">Matches</span>
<div class="hover-indicator"></div>
</a>
<a href="/tournaments/{tournamnentGuid}/settings" class="menu-item {currentPage === 'settings' ? 'active' : ''}">
<span class="menu-icon material-icons">settings</span>
<span class="menu-text">Settings</span>
<div class="hover-indicator"></div>
</a>
<a href="/tournaments/{tournamnentGuid}/qualifiers" class="menu-item {currentPage === 'qualifiers' ? 'active' : ''}">
<span class="menu-icon material-icons">filter_list</span>
<span class="menu-text">Qualifiers</span>
<div class="hover-indicator"></div>
</a>
<a href="/tournaments/{tournamnentGuid}/teams" class="menu-item {currentPage === 'teams' ? 'active' : ''}">
<span class="menu-icon material-icons">groups</span>
<span class="menu-text">Teams</span>
<div class="hover-indicator"></div>
</a>
<a href="/tournaments/{tournamnentGuid}/mappools" class="menu-item {currentPage === 'mappools' ? 'active' : ''}">
<span class="menu-icon material-icons">format_list_bulleted</span>
<span class="menu-text">Map Pools</span>
<div class="hover-indicator"></div>
</a>
</div>
<div class="menu-footer">
<div class="tournament-info">
<h3>Current Tournament</h3>
<p>{tournamentName || "No Name"}</p>
</div>
</div>
</div>
<style>
.side-menu {
width: 240px;
border-right: 1px solid var(--border-color);
display: flex;
flex-direction: column;
justify-content: space-between;
height: 100%;
}
.menu-items {
display: flex;
flex-direction: column;
padding: 1.5rem 0;
}
.menu-item {
display: flex;
align-items: center;
padding: 0.75rem 1.5rem;
color: var(--text-secondary);
text-decoration: none;
position: relative;
transition: color 0.2s ease;
transition: margin-left 0.2s ease;
}
.menu-item:hover {
color: var(--text-primary);
margin-left: 0.2rem;
}
.menu-item.active {
color: var(--text-primary);
}
.menu-icon {
margin-right: 0.75rem;
}
.menu-text {
font-weight: 500;
}
.menu-item .hover-indicator {
position: absolute;
left: 0;
top: 0;
width: 3px;
height: 0;
background: linear-gradient(180deg, transparent, var(--accent-color), transparent);
transition: height 0.3s ease;
opacity: 0;
}
.menu-item:hover .hover-indicator {
height: 100%;
opacity: 1;
box-shadow: 0 0 8px var(--accent-glow);
}
.menu-item.active .hover-indicator {
height: 100%;
opacity: 1;
box-shadow: 0 0 12px var(--accent-glow);
background: linear-gradient(180deg, transparent, var(--accent-hover), transparent);
}
.menu-footer {
padding: 1.5rem;
border-top: 1px solid var(--border-color);
}
.tournament-info h3 {
font-size: 0.875rem;
font-weight: 500;
color: var(--text-secondary);
margin-bottom: 0.5rem;
}
.tournament-info p {
font-weight: 600;
color: var(--text-primary);
}
</style>

View file

@ -0,0 +1,89 @@
<script lang="ts">
export let content: string;
export let onClose: () => void;
</script>
<div class="overlay">
<div class="popup">
<div class="popup-header">
<h3>Information Popup</h3>
<button class="close-button" on:click={onClose}>
<span class="material-icons">close</span>
</button>
</div>
<div class="popup-content">
{@html content}
</div>
</div>
</div>
<style>
.overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
}
.popup {
background-color: var(--bg-primary);
border-radius: 0.75rem;
padding: 1.5rem;
max-width: 30rem;
width: 90%;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.2);
position: relative;
}
.popup-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1rem;
}
.popup-header h3 {
margin: 0;
font-size: 1.25rem;
font-weight: 600;
color: var(--text-primary);
}
.close-button {
background: none;
border: none;
cursor: pointer;
padding: 0.25rem;
margin-left: 1rem;
color: #ff5555;
transition: color 0.2s;
}
.close-button:hover {
color: #ff0000;
}
.close-button .material-icons {
font-size: 1.5rem;
}
.popup-content {
color: var(--text-primary);
line-height: 1.5;
font-size: 0.9375rem;
}
.popup-content :global(p) {
margin: 0 0 1rem 0;
}
.popup-content :global(p:last-child) {
margin-bottom: 0;
}
</style>

View file

@ -0,0 +1,332 @@
<script lang="ts">
import { createEventDispatcher, onMount } from 'svelte';
import { fade } from 'svelte/transition';
// Define color presets
const colorPresets = {
primary: 'var(--accent-glow)',
secondary: 'var(--text-secondary)',
success: '#4CAF50',
warning: '#FFC107',
error: '#F44336',
info: '#2196F3',
default: 'var(--text-primary)'
};
type ButtonType = 'primary' | 'secondary' | 'text' | 'outlined';
type ColorPreset = keyof typeof colorPresets;
interface ButtonConfig {
text: string;
type: ButtonType;
href?: string;
color?: ColorPreset;
icon?: string;
action?: () => void;
}
// Props
export let open = false;
export let message: string = '';
export let icon: string = '';
export let iconColor: ColorPreset = 'primary';
export let buttons: ButtonConfig[] = [];
export let customImage: string = '';
export let autoClose: number = 0; // 0 means no auto close
// Event dispatcher
const dispatch = createEventDispatcher();
// Close the notification
function close() {
open = false;
dispatch('close');
}
// Handle button click
function handleButtonClick(button: ButtonConfig) {
if (button.action) {
button.action();
}
if (!button.href) {
close();
}
}
// Auto close functionality
onMount(() => {
if (autoClose > 0 && open) {
const timer = setTimeout(() => {
close();
}, autoClose);
return () => {
clearTimeout(timer);
};
}
});
// Get color from preset or return the input if not a preset
function getColor(colorName: ColorPreset): string {
return colorPresets[colorName] || colorName;
}
// Get button class based on type
function getButtonClass(type: ButtonType): string {
switch(type) {
case 'primary':
return 'btn-primary';
case 'secondary':
return 'btn-secondary';
case 'text':
return 'btn-text';
case 'outlined':
return 'btn-outlined';
default:
return 'btn-primary';
}
}
</script>
{#if open}
<!-- svelte-ignore a11y-no-static-element-interactions -->
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div class="notification-overlay" transition:fade={{ duration: 200 }} on:click={close}>
<!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div class="notification-container" on:click|stopPropagation>
<div class="notification-content">
{#if customImage}
<div class="custom-image">
{#if customImage.endsWith('.svg')}
{@html customImage}
{:else}
<!-- svelte-ignore a11y-img-redundant-alt -->
<img src={customImage} alt="Notification image" />
{/if}
</div>
{:else if icon}
<div class="notification-icon" style="color: {getColor(iconColor)}">
<span class="material-icons">{icon}</span>
</div>
{/if}
<div class="notification-message">
{message}
</div>
{#if buttons.length > 0}
<div class="notification-actions">
{#each buttons as button}
{#if button.href}
<a
href={button.href}
class="notification-button {getButtonClass(button.type)}"
style={button.color ? `--button-color: ${getColor(button.color)};` : ''}
>
{#if button.icon}
<span class="material-icons" style={button.color ? `color: ${getColor(button.color)};` : ''}>
{button.icon}
</span>
{/if}
<span>{button.text}</span>
</a>
{:else}
<button
class="notification-button {getButtonClass(button.type)}"
style={button.color ? `--button-color: ${getColor(button.color)};` : ''}
on:click={() => handleButtonClick(button)}
>
{#if button.icon}
<span class="material-icons" style={button.color ? `color: ${getColor(button.color)};` : ''}>
{button.icon}
</span>
{/if}
<span>{button.text}</span>
</button>
{/if}
{/each}
</div>
{/if}
</div>
</div>
</div>
{/if}
<style>
.notification-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
backdrop-filter: blur(3px);
}
.notification-container {
background-color: var(--bg-secondary);
border-radius: 8px;
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.3);
width: 90%;
max-width: 500px;
border: 1px solid var(--border-color);
overflow: hidden;
animation: popup 0.3s ease forwards;
}
@keyframes popup {
0% { transform: scale(0.9); opacity: 0; }
100% { transform: scale(1); opacity: 1; }
}
.notification-content {
padding: 2rem;
display: flex;
flex-direction: column;
align-items: center;
gap: 1.5rem;
}
.notification-icon {
font-size: 3rem;
display: flex;
justify-content: center;
align-items: center;
}
.notification-icon .material-icons {
font-size: 3.5rem;
text-shadow: 0 0 10px var(--accent-glow);
}
.custom-image {
max-width: 120px;
margin-bottom: 1rem;
}
.custom-image img {
width: 100%;
height: auto;
}
.notification-message {
text-align: center;
font-size: 1.25rem;
color: var(--text-primary);
line-height: 1.6;
}
.notification-actions {
display: flex;
gap: 1rem;
justify-content: center;
margin-top: 1rem;
flex-wrap: wrap;
}
.notification-button {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.75rem 1.5rem;
border-radius: 4px;
font-weight: 600;
font-size: 1rem;
transition: all 0.3s ease;
cursor: pointer;
text-decoration: none;
justify-content: center;
--button-color: var(--accent-color);
}
.notification-button .material-icons {
font-size: 1.25rem;
}
.btn-primary {
background-color: var(--button-color, var(--accent-color));
color: white;
border: none;
}
.btn-primary:hover {
background-color: var(--accent-hover);
box-shadow: 0 0 15px var(--accent-glow);
}
.btn-secondary {
background-color: transparent;
color: var(--button-color, var(--accent-color));
border: 1px solid var(--button-color, var(--accent-color));
}
.btn-secondary:hover {
background-color: rgba(var(--accent-color-rgb), 0.1);
}
.btn-text {
background-color: transparent;
color: var(--button-color, var(--accent-color));
border: none;
padding: 0.75rem 1rem;
}
.btn-text:hover {
background-color: rgba(var(--accent-color-rgb), 0.1);
}
.btn-outlined {
background-color: transparent;
color: var(--button-color, var(--accent-color));
border: 1px solid var(--button-color, var(--accent-color));
position: relative;
overflow: hidden;
}
.btn-outlined::after {
content: '';
position: absolute;
bottom: 0;
left: 0;
width: 100%;
height: 3px;
background: linear-gradient(90deg, transparent, var(--button-color, var(--accent-color)), transparent);
transform: translateX(-100%);
transition: transform 0.3s ease;
opacity: 0;
}
.btn-outlined:hover {
text-shadow: 0 0 8px var(--accent-glow);
}
.btn-outlined:hover::after {
transform: translateX(0);
opacity: 1;
box-shadow: 0 0 15px var(--accent-glow);
}
@media (max-width: 480px) {
.notification-content {
padding: 1.5rem;
}
.notification-message {
font-size: 1rem;
}
.notification-actions {
flex-direction: column;
width: 100%;
}
.notification-button {
width: 100%;
}
}
</style>

View file

@ -0,0 +1,528 @@
<script lang="ts">
import { createEventDispatcher } from "svelte";
import { TAClient } from "moons-ta-client";
const dispatch = createEventDispatcher();
export let isOpen: boolean = false;
export let taClient: TAClient;
export let tournamentGuid: string;
// User states
let discordUserId: string = "";
let isSearching: boolean = false;
let userFound: boolean = false;
let error: string | null = null;
// Discord user data
let discordUser: {
discordId: string;
discordUsername: string;
discordAvatarUrl: string;
} | null = null;
// Permission settings
let hasAdminPermission: boolean = false;
let hasViewPermission: boolean = true;
// This will be implemented by the parent component
async function fetchDiscordUser() {
// Placeholder function to be implemented externally
isSearching = true;
error = null;
try {
if(!taClient.isConnected) {
throw new Error("TA Client is not connected. Please refresh site.")
}
if (discordUserId.length >= 17) {
let user = await taClient.getDiscordInfo(tournamentGuid, discordUserId);
console.log(user);
discordUser = {
discordId: discordUserId,
discordUsername: (user as any).details.getDiscordInfo.discordUsername,
discordAvatarUrl: (user as any).details.getDiscordInfo.discordAvatarUrl
};
userFound = true;
} else {
throw new Error("Invalid Discord ID format");
}
} catch (err) {
error = err instanceof Error ? err.message : "Failed to fetch user";
userFound = false;
discordUser = null;
} finally {
isSearching = false;
}
}
function handleSearch() {
if (discordUserId.trim() !== "") {
fetchDiscordUser();
}
}
function handleKeyDown(event: KeyboardEvent) {
if (event.key === "Enter") {
handleSearch();
}
}
function resetForm() {
discordUserId = "";
discordUser = null;
userFound = false;
error = null;
hasAdminPermission = false;
hasViewPermission = true;
}
function closePopup() {
resetForm();
dispatch("close");
}
function addUser() {
if (discordUser) {
let permission;
enum Permissions {
"None" = 0,
"View Only" = 1,
"Administrator" = 2,
"View and Administrator" = 3
}
if(hasAdminPermission && hasViewPermission) {
permission = Permissions["View and Administrator"];
} else if(hasViewPermission) {
permission = Permissions["View Only"];
} else if(hasAdminPermission) {
permission = Permissions.Administrator;
} else {
permission = Permissions.None;
}
dispatch("addUser", {
discordId: discordUser.discordId,
discordUsername: discordUser.discordUsername,
discordAvatarUrl: discordUser.discordAvatarUrl,
permission
});
closePopup();
}
}
</script>
{#if isOpen}
<div class="popup-overlay">
<div class="popup-container">
<div class="popup-header">
<h3>Add User</h3>
<button class="close-button" on:click={closePopup}>
<span class="material-icons">close</span>
</button>
</div>
<div class="popup-content">
<div class="search-section">
<label for="discord-id">Discord User ID</label>
<div class="search-input-container">
<input
type="text"
id="discord-id"
bind:value={discordUserId}
placeholder="Enter Discord User ID"
on:keydown={handleKeyDown}
disabled={isSearching || userFound}
/>
{#if !userFound}
<button
class="search-button"
on:click={handleSearch}
disabled={isSearching || discordUserId.trim() === ""}
>
{#if isSearching}
<div class="spinner-small"></div>
{:else}
<span class="material-icons">search</span>
{/if}
</button>
{:else}
<button class="reset-button" on:click={resetForm}>
<span class="material-icons">refresh</span>
</button>
{/if}
</div>
{#if error}
<p class="error-message">{error}</p>
{/if}
</div>
{#if userFound && discordUser}
<div class="user-result">
<div class="user-preview">
<img
src={discordUser.discordAvatarUrl}
alt={discordUser.discordUsername}
class="user-avatar"
/>
<div class="user-info">
<h4>{discordUser.discordUsername}</h4>
<p class="user-id">ID: {discordUser.discordId}</p>
</div>
</div>
<div class="permissions-section">
<h4>Permissions</h4>
<div class="permission-option">
<div class="permission-label">
<label for="view-permission">View Permission</label>
<p class="permission-description">Can view tournament data (generally players)</p>
</div>
<label class="toggle">
<input type="checkbox" id="view-permission" bind:checked={hasViewPermission}>
<span class="slider"></span>
</label>
</div>
<div class="permission-option">
<div class="permission-label">
<label for="admin-permission">Admin Permission</label>
<p class="permission-description">Can manage tournament settings (generally staff))</p>
</div>
<label class="toggle">
<input type="checkbox" id="admin-permission" bind:checked={hasAdminPermission}>
<span class="slider"></span>
</label>
</div>
</div>
</div>
{/if}
</div>
<div class="popup-actions">
<button class="cancel-button" on:click={closePopup}>Cancel</button>
<button
class="add-button"
on:click={addUser}
disabled={!userFound || (!hasViewPermission && !hasAdminPermission)}
>
Add User
</button>
</div>
</div>
</div>
{/if}
<style>
.popup-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
}
.popup-container {
background-color: var(--bg-primary);
border-radius: 0.75rem;
width: 90%;
max-width: 32rem;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.2);
display: flex;
flex-direction: column;
}
.popup-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1.5rem;
border-bottom: 1px solid var(--bg-tertiary);
}
.popup-header h3 {
margin: 0;
font-size: 1.25rem;
font-weight: 600;
}
.close-button {
background: none;
border: none;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
color: var(--text-secondary);
padding: 0.5rem;
border-radius: 0.375rem;
}
.close-button:hover {
background-color: var(--bg-tertiary);
color: var(--text-primary);
}
.popup-content {
padding: 1.5rem;
flex: 1;
display: flex;
flex-direction: column;
gap: 1.5rem;
}
.search-section {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.search-section label {
font-weight: 500;
font-size: 0.9375rem;
}
.search-input-container {
display: flex;
gap: 0.5rem;
}
.search-input-container input {
flex: 1;
background-color: var(--bg-tertiary);
border: none;
border-radius: 0.375rem;
padding: 0.625rem 0.75rem;
color: var(--text-primary);
font-size: 0.9375rem;
}
.search-input-container input:focus {
outline: 2px solid var(--accent-color);
}
.search-button,
.reset-button {
background-color: var(--accent-color);
border: none;
border-radius: 0.375rem;
width: 2.5rem;
display: flex;
align-items: center;
justify-content: center;
color: white;
cursor: pointer;
}
.search-button:hover,
.reset-button:hover {
background-color: var(--accent-hover);
}
.search-button:disabled {
background-color: var(--bg-tertiary);
color: var(--text-secondary);
cursor: not-allowed;
}
.error-message {
color: #ff5555;
font-size: 0.875rem;
margin: 0.5rem 0 0 0;
}
.user-result {
display: flex;
flex-direction: column;
gap: 1.5rem;
margin-top: 0.5rem;
}
.user-preview {
display: flex;
align-items: center;
gap: 1rem;
background-color: var(--bg-tertiary);
border-radius: 0.5rem;
padding: 1rem;
}
.user-avatar {
width: 3rem;
height: 3rem;
border-radius: 50%;
object-fit: cover;
}
.user-info {
flex: 1;
}
.user-info h4 {
margin: 0;
font-size: 1rem;
font-weight: 500;
}
.user-id {
margin: 0.25rem 0 0 0;
font-size: 0.8125rem;
color: var(--text-secondary);
}
.permissions-section {
display: flex;
flex-direction: column;
gap: 1rem;
}
.permissions-section h4 {
margin: 0 0 0.5rem 0;
font-size: 1rem;
font-weight: 500;
}
.permission-option {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.75rem;
background-color: var(--bg-tertiary);
border-radius: 0.5rem;
}
.permission-label {
flex: 1;
}
.permission-label label {
font-weight: 500;
font-size: 0.9375rem;
display: block;
margin-bottom: 0.25rem;
}
.permission-description {
margin: 0;
font-size: 0.8125rem;
color: var(--text-secondary);
}
/* Toggle styles (reusing from existing code) */
.toggle {
position: relative;
display: inline-block;
width: 3rem;
height: 1.5rem;
color: grey;
}
.toggle input {
opacity: 0;
width: 0;
height: 0;
}
.slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: var(--bg-secondary);
transition: .4s;
border-radius: 1.5rem;
}
.slider:before {
position: absolute;
content: "";
height: 1.125rem;
width: 1.125rem;
left: 0.1875rem;
bottom: 0.1875rem;
background-color: var(--text-secondary);
transition: .4s;
border-radius: 50%;
}
input:checked + .slider {
background-color: var(--accent-color);
}
input:checked + .slider:before {
transform: translateX(1.5rem);
background-color: white;
}
.popup-actions {
display: flex;
justify-content: flex-end;
gap: 1rem;
padding: 1.25rem 1.5rem;
border-top: 1px solid var(--bg-tertiary);
}
.cancel-button {
background-color: var(--bg-tertiary);
border: none;
border-radius: 0.375rem;
padding: 0.625rem 1.25rem;
font-size: 0.9375rem;
font-weight: 500;
color: var(--text-secondary);
cursor: pointer;
}
.cancel-button:hover {
background-color: var(--bg-secondary);
color: var(--text-primary);
}
.add-button {
background-color: var(--accent-color);
border: none;
border-radius: 0.375rem;
padding: 0.625rem 1.25rem;
font-size: 0.9375rem;
font-weight: 500;
color: white;
cursor: pointer;
}
.add-button:hover {
background-color: var(--accent-hover);
}
.add-button:disabled {
background-color: var(--bg-tertiary);
color: var(--text-secondary);
cursor: not-allowed;
}
.spinner-small {
width: 1.25rem;
height: 1.25rem;
border: 0.125rem solid rgba(255, 255, 255, 0.3);
border-radius: 50%;
border-top-color: white;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
</style>

View file

@ -0,0 +1,345 @@
<script lang="ts">
import { createEventDispatcher } from 'svelte';
import { v4 as uuidv4 } from 'uuid';
import { convertImageToUint8Array } from "$lib/services/taImages.js";
const dispatch = createEventDispatcher();
// Map pool data
let mapPoolName: string = "";
let mapPoolImageBuffer: Uint8Array | null = null;
let mapPoolImagePreview: string | null = null;
// File upload
let fileInput: HTMLInputElement;
function closeModal() {
dispatch('close');
}
function handleImageUpload() {
fileInput.click();
}
function onFileSelected(event: Event) {
const target = event.target as HTMLInputElement;
const files = target.files;
if (files && files.length > 0) {
const file = files[0];
const reader = new FileReader();
reader.onload = (e) => {
const result = e.target?.result;
if (result instanceof ArrayBuffer) {
mapPoolImageBuffer = new Uint8Array(result);
// Create preview URL
const blob = new Blob([mapPoolImageBuffer], { type: file.type });
mapPoolImagePreview = URL.createObjectURL(blob);
}
};
reader.readAsArrayBuffer(file);
}
}
async function createMapPool() {
// Create a default image buffer if none provided
let imageBuffer = mapPoolImageBuffer;
if (!imageBuffer) {
imageBuffer = new Uint8Array(1);
}
// Create the map pool object
const newMapPool = {
guid: uuidv4(),
name: mapPoolName,
image: imageBuffer,
maps: []
};
// Dispatch event with map pool data
dispatch('mapPoolCreated', newMapPool);
// Close the modal
closeModal();
}
</script>
<div class="modal-overlay">
<div class="modal-container">
<div class="modal-header">
<h3>Create New Map Pool</h3>
<button class="close-button" on:click={closeModal}>
<span class="material-icons">close</span>
</button>
</div>
<div class="modal-content">
<div class="form-group">
<label for="mappool-name">Map Pool Name</label>
<input
type="text"
id="mappool-name"
class="form-input"
bind:value={mapPoolName}
placeholder="Enter map pool name"
required
/>
</div>
<div class="form-group">
<!-- svelte-ignore a11y-label-has-associated-control -->
<label>Map Pool Logo (Optional)</label>
<div class="image-upload-container">
<div class="image-preview">
{#if mapPoolImagePreview}
<img src={mapPoolImagePreview} alt="Map pool logo preview" />
{:else}
<!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-noninteractive-element-interactions -->
<img on:click={handleImageUpload} src="/talogo.png" alt="Default logo" />
<!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div on:click={handleImageUpload} class="upload-overlay">
<span class="material-icons">add_photo_alternate</span>
</div>
{/if}
</div>
<button class="action-button upload" on:click={handleImageUpload}>
<span class="material-icons">upload</span>
{mapPoolImagePreview ? 'Change Logo' : 'Upload Logo'}
</button>
<input
type="file"
accept="image/*"
style="display: none"
bind:this={fileInput}
on:change={onFileSelected}
/>
<p class="helper-text">If no image is provided, the default logo will be used.</p>
</div>
</div>
<div class="modal-actions">
<button class="action-button cancel" on:click={closeModal}>Cancel</button>
<button
class="action-button create"
on:click={createMapPool}
disabled={!mapPoolName}
>
Create Map Pool
</button>
</div>
</div>
</div>
</div>
<style>
.modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
}
.modal-container {
background-color: var(--bg-primary);
border-radius: 0.75rem;
width: 90%;
max-width: 500px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.2);
animation: modal-appear 0.3s ease-out;
}
@keyframes modal-appear {
from {
opacity: 0;
transform: translateY(-20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1.25rem 1.5rem;
border-bottom: 1px solid var(--bg-secondary);
}
.modal-header h3 {
margin: 0;
font-size: 1.25rem;
font-weight: 600;
}
.close-button {
background: none;
border: none;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
color: var(--text-secondary);
width: 2rem;
height: 2rem;
border-radius: 0.25rem;
transition: all 0.2s;
}
.close-button:hover {
background-color: var(--bg-secondary);
color: var(--text-primary);
}
.modal-content {
padding: 1.5rem;
}
.form-group {
margin-bottom: 1.25rem;
}
.form-group label {
display: block;
margin-bottom: 0.5rem;
font-weight: 500;
font-size: 0.9375rem;
}
.form-input {
width: 100%;
padding: 0.625rem 0.75rem;
background-color: var(--bg-tertiary);
border: none;
border-radius: 0.375rem;
color: var(--text-primary);
font-size: 0.9375rem;
font-family: inherit;
transition: outline 0.2s;
}
.form-input:focus {
outline: 2px solid var(--accent-color);
}
/* Image upload in modal */
.image-upload-container {
display: flex;
flex-direction: column;
gap: 1rem;
align-items: flex-start;
}
.image-preview {
width: 6rem;
height: 6rem;
border-radius: 0.375rem;
overflow: hidden;
background-color: var(--bg-tertiary);
position: relative;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.image-preview img {
width: 100%;
height: 100%;
object-fit: cover;
}
.upload-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: opacity 0.2s;
}
.upload-overlay:hover {
opacity: 0.9;
}
.upload-overlay .material-icons {
color: #fff;
font-size: 1.5rem;
}
.helper-text {
font-size: 0.75rem;
color: var(--text-secondary);
margin: 0.25rem 0 0 0;
}
.modal-actions {
display: flex;
justify-content: flex-end;
gap: 0.75rem;
margin-top: 1.5rem;
}
.action-button {
display: flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
padding: 0.625rem 1rem;
font-size: 0.875rem;
font-weight: 500;
border-radius: 0.375rem;
cursor: pointer;
transition: all 0.2s;
border: none;
}
.action-button.upload {
background-color: var(--bg-secondary);
color: var(--text-primary);
}
.action-button.upload:hover {
background-color: var(--bg-tertiary);
}
.action-button.cancel {
background-color: var(--bg-secondary);
color: var(--text-primary);
}
.action-button.cancel:hover {
background-color: var(--bg-tertiary);
}
.action-button.create {
background-color: var(--accent-color);
color: white;
}
.action-button.create:hover {
opacity: 0.9;
}
.action-button.create:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.action-button .material-icons {
font-size: 1.25rem;
}
</style>

File diff suppressed because it is too large Load diff

6
src/lib/config.json Normal file
View file

@ -0,0 +1,6 @@
{
"discordAuthUrl": "https://discord.com/oauth2/authorize?client_id=1348291960552820757&response_type=token&redirect_uri=http%3A%2F%2Flocalhost%3A1420%2FdiscordAuth&scope=identify",
"bkAPIUrl": "https://api.beatkhana.com/api",
"prodAuthUrl": "https://discord.com/oauth2/authorize?client_id=1348291960552820757&response_type=token&redirect_uri=https%3A%2F%2Ftaui.shyyluna.dev%2FdiscordAuth&scope=identify",
"stagingAuthUrl": "https://discord.com/oauth2/authorize?client_id=1348291960552820757&response_type=token&redirect_uri=https%3A%2F%2Fstaging.taui.shyyluna.dev%2FdiscordAuth&scope=identify"
}

View file

@ -0,0 +1,14 @@
/**
* Generates a random hex color code with the # prefix.
* @returns A string representing a random hex color code (e.g. "#1A2B3C").
*/
export function generateRandomHexColor(): string {
// Generate a random number between 0 and 16777215 (decimal for FFFFFF)
const randomNumber: number = Math.floor(Math.random() * 16777215);
// Convert the number to a hexadecimal string and pad with zeros if needed
const hexString: string = randomNumber.toString(16).padStart(6, '0');
// Return the hex color with # prefix
return `#${hexString}`;
}

85
src/lib/services/jwt.ts Normal file
View file

@ -0,0 +1,85 @@
/**
* Validates a JWT token and checks if it has expired
* @param token The JWT token to validate
* @returns An object containing validation status and decoded token if valid
*/
export function validateJWT(token: string): {
valid: boolean;
expired: boolean;
decoded?: { [key: string]: any };
error?: string
} {
try {
// Check if token exists
if (!token) {
return { valid: false, expired: false, error: 'No token provided' };
}
// Split the token into parts
const parts = token.split('.');
if (parts.length !== 3) {
return { valid: false, expired: false, error: 'Invalid token format' };
}
// Decode the payload (middle part)
const payload = JSON.parse(Buffer.from(parts[1], 'base64').toString());
// Check expiration
const currentTime = Math.floor(Date.now() / 1000);
if (payload.exp && payload.exp < currentTime) {
return {
valid: false,
expired: true,
decoded: payload,
error: 'Token has expired'
};
}
// Check if token is not valid before a specific time (if applicable)
if (payload.nbf && payload.nbf > currentTime) {
return {
valid: false,
expired: false,
decoded: payload,
error: 'Token not yet valid'
};
}
// If we reach here, token is valid
return { valid: true, expired: false, decoded: payload };
} catch (error) {
return {
valid: false,
expired: false,
error: error instanceof Error ? error.message : 'Unknown error parsing token'
};
}
}
/**
* Helper function to get and validate JWT from localStorage
* @returns The validation result containing token status
*/
export function getAndValidateJWT(): {
valid: boolean;
expired: boolean;
decoded?: { [key: string]: any };
error?: string;
token?: string;
} {
try {
const token = localStorage.getItem('jwt_token');
if (!token) {
return { valid: false, expired: false, error: 'No token stored' };
}
const result = validateJWT(token);
return { ...result, token };
} catch (error) {
return {
valid: false,
expired: false,
error: error instanceof Error ? error.message : 'Error retrieving token'
};
}
}

View file

@ -0,0 +1,156 @@
/**
* Converts a Uint8Array buffer to a usable image URL
* @param imageBuffer The Uint8Array containing image data
* @param mimeType The MIME type of the image (default: 'image/png')
* @returns A data URL or Blob URL that can be used as an image source
*/
export function bufferToImageUrl(imageBuffer: Uint8Array, mimeType: string = 'image/png'): string {
if(imageBuffer.length == 1) {
throw new Error("This is the default logo.")
}
// Method 1: Create a data URL
const base64String = arrayBufferToBase64(imageBuffer);
return `data:${mimeType};base64,${base64String}`;
// Alternative Method 2: Create a Blob URL
// const blob = new Blob([imageBuffer], { type: mimeType });
// return URL.createObjectURL(blob);
}
/**
* Helper function to convert ArrayBuffer to base64 string
* @param buffer The ArrayBuffer to convert
* @returns Base64 encoded string
*/
export function arrayBufferToBase64(buffer: Uint8Array): string {
let binary = '';
const bytes = new Uint8Array(buffer);
const len = bytes.byteLength;
for (let i = 0; i < len; i++) {
binary += String.fromCharCode(bytes[i]);
}
return btoa(binary);
}
export async function convertImageToUint8Array(file: File): Promise<Uint8Array> {
return new Promise((resolve, reject) => {
// Create a FileReader to read the file
const reader = new FileReader();
// Set up the onload event handler
reader.onload = function() {
// Create an image element to load the file data
const img = new Image();
img.onload = function() {
// Create a canvas to draw the image
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
if (!ctx) {
reject(new Error('Failed to get canvas context'));
return;
}
// Set canvas dimensions to match image
canvas.width = img.width;
canvas.height = img.height;
// Draw the image on the canvas
ctx.drawImage(img, 0, 0);
// Convert the canvas to a JPEG blob
canvas.toBlob((blob) => {
if (!blob) {
reject(new Error('Failed to create blob'));
return;
}
// Convert blob to ArrayBuffer
const blobReader = new FileReader();
blobReader.onload = function() {
if (!blobReader.result || typeof blobReader.result === 'string') {
reject(new Error('Failed to read blob data'));
return;
}
// Convert ArrayBuffer to Uint8Array
const uint8Array = new Uint8Array(blobReader.result);
resolve(uint8Array);
};
blobReader.onerror = function() {
reject(new Error('Failed to read blob'));
};
blobReader.readAsArrayBuffer(blob);
}, 'image/jpeg', 0.85); // 0.85 is the quality level (0-1)
};
img.onerror = function() {
reject(new Error('Failed to load image'));
};
// Load the image with the file data
img.src = reader.result as string;
};
reader.onerror = function() {
reject(new Error('Failed to read file'));
};
// Read the file as a data URL
reader.readAsDataURL(file);
});
}
/**
* Converts a link (data URL or HTTP/HTTPS URL) to a Uint8Array
* @param link The image link (data URL or HTTP/HTTPS URL)
* @returns Promise resolving to a Uint8Array containing the image data
*/
export async function linkToUint8Array(link: string): Promise<Uint8Array> {
// Check if the link is a data URL
if (link.startsWith('data:')) {
// Extract the base64 part of the data URL
const matches = link.match(/^data:([^;]+);base64,(.+)$/);
if (!matches || matches.length !== 3) {
throw new Error('Invalid data URL format');
}
// Convert base64 to Uint8Array
const base64Data = matches[2];
const binaryString = atob(base64Data);
const length = binaryString.length;
const bytes = new Uint8Array(length);
for (let i = 0; i < length; i++) {
bytes[i] = binaryString.charCodeAt(i);
}
return bytes;
}
// Handle HTTP/HTTPS URLs
else if (link.startsWith('http://') || link.startsWith('https://')) {
try {
// Fetch the image
const response = await fetch(link);
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
// Get the array buffer from the response
const arrayBuffer = await response.arrayBuffer();
// Convert to Uint8Array
return new Uint8Array(arrayBuffer);
} catch (error) {
throw new Error(`Failed to fetch image: ${error instanceof Error ? error.message : String(error)}`);
}
} else {
throw new Error('Unsupported link format. Must be a data URL or HTTP/HTTPS URL');
}
}

27
src/lib/stores.js Normal file
View file

@ -0,0 +1,27 @@
import { TAClient } from 'moons-ta-client';
import { writable } from 'svelte/store';
// @ts-ignore
function createPersistedStore(key, startValue) {
const storedValue = localStorage.getItem(key);
const store = writable(storedValue ? JSON.parse(storedValue) : startValue);
store.subscribe(value => {
localStorage.setItem(key, JSON.stringify(value));
});
return store;
}
// set stores and their inital values
export const discordDataStore = createPersistedStore('discordDataTAUI', null);
export const discordTokenStore = createPersistedStore('discordAuthTAUI', null);
export const authTokenStore = createPersistedStore('authTokenTAUI', null);
export const TABotTokenStore = createPersistedStore('TABotTokenTAUI', null);
export const TAServerUrl = createPersistedStore('TAServerUrl', "server.tournamentassistant.net");
export const TAServerPort = createPersistedStore('TAServerPort', "8676");

95
src/lib/taDocs/client.md Normal file
View file

@ -0,0 +1,95 @@
# The TA Client
### This section assumes that you have followed the instalation guide in the introduction.
The table below shows all of the functions of the taClient. From this point onwards `taClient` will be defined as `let taClient = new TAClient();`.
| taClient. (function) | Purpose and functionality | Is Async |
|:--------------------:|:--------------------------|:--------:|
|addAuthorizedUser()|Add a user who as either an admin or a viewer to your TA server.|True|
|addQualifierMaps()|Add maps to a qualifier. This takes in an array of maps.|True|
|addServer()|Add a CoreServer. You will probably never use this.|True|
|addTournamentPool()|Add a map pool to a tournament. This is the way a map pool can be created.|True|
|addTournamentPoolMaps()|Add maps to an already existing map pool.|True|
|addTournamentTeam()|Add a team to a tournament. This will create a new team|True|
|addUserToMatch()|Add a user to a match. This is how a coordinator joins a match.|True|
|connect()|Connect to a TournamentAssistant Core Server. This is the main server, not the tournament.|True|
|createMatch()|Create a match.|True|
|createQualifierEvent()|Create a qualifier. This will only be shows to players if the show qualifiers button is enabled.|True|
|createTournament()|Create a new tournament. You will have to provide the unit8 array for the image.|True|
|delayTestFinished()|**STREAMSYNC MORE DOCS REQUIRED** Set the clients as ready to play on the server?|True|
|deleteMatch()|Delete a match.|True|
|deleteQualifierEvent()|Delete a qualifier event.|True|
|deleteTournament()|Delete a tournament.|True|
|disconnect()|Disconnect from the Core Server.|True|
|emit()|Emit a custom packet. This is used for new feature testing and will not be discussed|True|
|flipColors()|Flip the colours of the players in the match. This was used in JJ25, and has just been left in.|True|
|flipHands()|Flip the hands of the players. This will flip the left or right handed mode.|True|
|generateBotToken()|Generate a bot JWT. This is often used for overlays etc.|True|
|getAuthorizedUsers()|Get the authorised user array for a tournament.|True|
|getBotTokensForUser()|Get the bot tokens of a user. This does not return the JWT.|True|
|getDiscordInfo()|Get the discord information of a user.|True|
|getLeaderboard()|Get the leaderboard of a map in a qualifer.|True|
|isConnected()|Check if the taClient is connected to the Core Server.|False|
|isConnecting()|Check if the taCleint is currectly connecting to the Core Server|False|
|joinTournament()|Join a tournament as the specified user.|True|
|loadImage()|**STREAMSYNC MORE DOCS REQUIRED** Set the colour image for users in streamsync?|True|
|loadSong()|**does this load for specified users, or for a match**Load a song to the users in a match.|True|
|on()|This is the standard listener. This is how events such as real time score can be subscribed to.|True|
|once()|**PROBABLY THE SAME AS .ON()???**|True|
|playSong()|Play the currently loaded song for the specified players.|True|
|removeAuthorizedUser()|Remove an authorised user from a tournament.|True|
|removeListener()|Remove a listener which was previously subscribed to. Do this when your application terminates.|True|
|removeQualifierMap()|Remove a qualifier map from an already existing qualifier.|True|
|removeTournamentPool()|Remove a map pool from a tournament.|True|
|removeTournamentPoolMap()|Remove a map from an already existing map pool.|True|
|removeTournamentTeam()|Remove a team from a tournament.|True|
|removeUserFromMatch()|Remove a user from a match.|True|
|returnToMenu()|Return the speciied players to the menu.|False|
|revokeBotToken()|Revoke a bot token. This will revoke the access of a bot token.|True|
|sendResponse()|Send a custom response to the TA Core Server. Do not use this unless you know exactly what you are doing.|True|
|setAuthToken()|Set the authorisation token of the taClient.|True|
|setMatchLeader()|Set the leader of a match. This is usually supposed to be the coordinator of the match.|True|
|setMatchMap()|Set the map of a match. This will load the map for the players in the match.|True|
|setQualifierFlags()|**A BIT UNCLEAR ON WHAT THIS DOES** Set the settings for a qualifier.|True|
|setQualifierImage()|Set the image of a qualifier.|True|
|setQualifierInfoChannel()|Set the Discord channel where the qualifier scores can be sent by the TA bot.|True|
|setQualifierLeaderboardSort()|Set the sorting type of the qualifiers leaderboard.|True|
|setQualifierName()|Set the name of a qualifier.|True|
|setTournamentAllowUnauthorizedView()|Set whether the tournament may be viewed by people without the view permission.|True|
|setTournamentBannedMods()|Set the banned mods for a tournament.|True|
|setTournamentEnablePools()|Set whether the map pools feature is enabled.|True|
|setTournamentEnableTeams()|Set whether the teams feature is enabled.|True|
|setTournamentImage()|Set the image of the tournament.|True|
|setTournamentName()|Set the name of the tournament.|True|
|setTournamentPoolName()|Set the name of an already existing map pool in a tournament.|True|
|setTournamentScoreUpdateFrequency()|Set the score update frequency for a tournament. This is usualy 30 frames.|True|
|setTournamentShowQualifierButton()|Set whether the qualifier button is shown in the tournament menu in game.|True|
|setTournamentShowTournamentButton()|Set whether the tournament button is shown in the tournament menu in game.|True|
|setTournamentTeamImage()|Set the image of an already existing team in a tournament.|True|
|setTournamentTeamName()|Set the name of an already existing team in a tournament.|True|
|showLoadedImage()|**STREAMSYNC MORE DOCS REQUIRED** Show the loaded image that is currently used for streamsync.|True|
|showPrompt()|Show a custom prompt to the users.|True|
|stateManager|This is the state manager. Find further documentation, as it is a vital component.|True|
|updateQualifierMap()|Update the settings of an already existing qualifier map. |True|
|updateTournamentPoolMap()|Update the settings of a map already in a map pool.|True|
|updateUser()|**what does this do??? Maybe update the discord Id or what?**Update a user????|True|
These are the main functions of the taClient. It is important to note that these are mostly asyncronous and must be awaited for if you wish to use the response they provide. There is however another important part of the taClient, which is the `taClient.stateManger`. This `stateManger` also has more functions which I have also provided in a table below. The `stateManger` loads its data once it connects to the TA CoreServer.
| taClient.stateManager. (function) | Purpose and functionality | Is Async |
|:----------------------------------:|:--------------------------|:--------:|
|emit()|Emit a custom event. Only use this if you know what you are doing.|True|
|getKnownServers()|Get the known servers which have been sent in the connect state. **This is a cached list.**|False|
|getMatch()|Get the details of a match.|False|
|getMatches()|Get an array of all of the matches in a tournament.|False|
|getQualifier()|Get the details of a qualifier event.|False|
|getQualifiers()|Get an array of all of the qualifiers in a tournament.|False|
|getSelfGuid()|Get the `.guid` property of the currently logged in user or bot token.|False|
|getTournament()|Get the information about a tournament.|False|
|getTournaments()|Get an array of all of the tournaments of a Core Server|False|
|getUser()|Get all of the details of a user in a tournament.|False|
|getUsers()|Get an array of all of the users in a tournament.|False|
|handlePacket()|Handle the response to a packet sent to you by the TA Core Server. Only use if you know what you are doing.|False|
|**on()**|Standard listener for the taClient. This is how real time score can be subscribed to.|False|
|once()|**SAME AS ON? WHAT IS THE DIFFERENCE BETWEEN TACLIENT.NO AND STATEMANAGER.ON????**|False|
|removeListener()|**why can I subscribe to events in the statemanager? I am quite unclear about this hehe**|False|

67
src/lib/taDocs/intro.md Normal file
View file

@ -0,0 +1,67 @@
# Introduction
TournamentAssistant is a piece of woftware which has a backend(TournamentAssistantServer), frontend(TournamentAssistantUI), and in-game plugin(TournamentAssistant). It is important to note before proceeding that TA uses a websocket for connections and DOES NOT have an API. The websocket uses protobuf, so the packets themseves are not readable by humans naturally. That is why the TA client is important. It includes the proto models and deserialises them.
## Installing the npm package
The client can be installed through the npm repository and has a crucial role in transforming packets and seding them to the websocket.
```code
npm i moons-ta-client
```
Once installed we can run some basic scripts in any JS/TS file. I strongly recommend the use of TS, as it is what I will be using in the examples which I provide.
## Let's run a basic script
We will construct a script which will connect to the default TournamentAssistant server and get the information of a tournament.
```ts
import { Response_ResponseType, TAClient } from 'moons-ta-client';
const taClient: TAClient = new TAClient();
taClient.setAuthToken("JWT HERE");
const connectResult = await taClient.connect('server.tournamentassistant.net', '8676');
if (connectResult.details.oneofKind !== "connect" || connectResult.type === Response_ResponseType.Fail) {
throw new Error(connectResult.details.connect.message);
}
taClient.disconnect();
```
You might notice that we have not actually connected to a tournament or fetched any details. This is intentional, as the connectResult already inclides the current state of the given TA server.
<sub>As of now, it is not possible to host your own TA server. This is not the intention of the new TA, rather it is meant to centralise everything as it is difficult for everyone to swap servers constantly.</sub>
The connectResult has the following schema:
```ts
{
type: Response_ReaponseType,
respondingToPacketId: 'Packet-Guid',
details: {
oneofKind: 'connect',
connect: { serverVersion: 118, message: '', reason: 0, state: [Object] }
}
}
```
The state object includes the current list of tournaments, hence they do not need to be fetched again.
The state object looks as follows:
```ts
{
tournaments: [
{
guid: '97a9de91-646b-409e-b6ea-be5ad7b9a27e',
users: [],
matches: [],
qualifiers: [],
settings: [Object],
server: [Object]
}
],
knownServers: [
{
name: 'Default Server',
address: 'server.tournamentassistant.net',
port: 8675,
websocketPort: 8676
}
]
}
```
`connectResult.details.connect.knownServers` includes all of the main TA servers which are currently running. In the future it may be that BSWC or some large tournaments opt to run their own TA servers, however this is highly unlikely as it is not the direction TA is going.
`connectResult.details.connect.tournaments` is an array of all of the tournaments on the server which you just connected to. It includes the `guid` of the tournament, which we will use to join it and perform other operations. It also includes the `users` array which is going to play a crucial role in determining players etc. Note that the `users` array includes ALL users. It includes bot users, coordinators, players in matches, players in the lobby, anyone connected. We will get to the other properties soon, however it is best if you jump to the section with the tournament proto model.

21
src/lib/taDocs/sample.md Normal file
View file

@ -0,0 +1,21 @@
# This is a main header.
This is just some normal text that must have some spooky notes to have that info box ;)
## This is a heading 2
Some more normal text...
<div class="info-box">
<span class="material-icons">info</span>
<div>
<strong>Note:</strong> You'll need to connect your Discord account to use all features of Tournament Assistant.
</div>
</div>
## Support
If you have any issues, please contact us through Discord or email
- Email: support@tournamentassistant.net
- Discord: Join our community server at [discord.gg/tournament](https://discord.gg/tournament)

View file

@ -0,0 +1,171 @@
# The Tournament Model
Since ProtoBuf is used, the Tournament Model can also be found within the proto models. You can find it in `node_modules/moons-ta-client/dist/models/models.d.ts`. The standard Tournament Model is:
```ts
export interface Tournament {
/**
* @generated from protobuf field: string guid = 1;
*/
guid: string;
/**
* @generated from protobuf field: proto.models.Tournament.TournamentSettings settings = 2;
*/
settings?: Tournament_TournamentSettings;
/**
* @generated from protobuf field: repeated proto.models.User users = 3;
*/
users: User[];
/**
* @generated from protobuf field: repeated proto.models.Match matches = 4;
*/
matches: Match[];
/**
* @generated from protobuf field: repeated proto.models.QualifierEvent qualifiers = 5;
*/
qualifiers: QualifierEvent[];
/**
* @generated from protobuf field: proto.models.CoreServer server = 6;
*/
server?: CoreServer;
}
```
Let me also provide the interfaces for settings, users, and the other fields.
```ts
export interface Tournament_TournamentSettings {
tournamentName: string;
tournamentImage: Uint8Array;
enableTeams: boolean;
enablePools: boolean;
teams: Tournament_TournamentSettings_Team[];
scoreUpdateFrequency: number;
bannedMods: string[];
pools: Tournament_TournamentSettings_Pool[];
showTournamentButton: boolean;
showQualifierButton: boolean;
allowUnauthorizedView: boolean;
}
export interface Tournament_TournamentSettings_Pool {
guid: string;
name: string;
image: Uint8Array;
maps: Map[];
}
export interface Tournament_TournamentSettings_Team {
guid: string;
name: string;
image: Uint8Array;
}
export interface User {
guid: string;
name: string;
platformId: string;
clientType: User_ClientTypes;
teamId: string;
playState: User_PlayStates;
downloadState: User_DownloadStates;
modList: string[];
streamScreenCoordinates?: User_Point;
streamDelayMs: bigint;
streamSyncStartMs: bigint;
discordInfo?: User_DiscordInfo;
userImage: Uint8Array;
permissions: Permissions;
}
export interface User_DiscordInfo {
userId: string;
username: string;
avatarUrl: string;
}
export interface User_Point {
x: number;
y: number;
}
export declare enum User_PlayStates {
InMenu = 0,
WaitingForCoordinator = 1,
InGame = 2
}
export declare enum User_DownloadStates {
None = 0,
Downloading = 1,
Downloaded = 2,
DownloadError = 3
}
export declare enum User_ClientTypes {
Player = 0,
WebsocketConnection = 1
}
export interface Match {
guid: string;
associatedUsers: string[];
leader: string;
selectedMap?: Map;
}
export interface QualifierEvent {
guid: string;
name: string;
image: Uint8Array;
infoChannel?: Channel;
qualifierMaps: Map[];
flags: QualifierEvent_EventSettings;
sort: QualifierEvent_LeaderboardSort;
}
export declare enum QualifierEvent_EventSettings {
None = 0,
HideScoresFromPlayers = 1,
DisableScoresaberSubmission = 2,
EnableDiscordScoreFeed = 4,
EnableDiscordLeaderboard = 8
}
export declare enum QualifierEvent_LeaderboardSort {
ModifiedScore = 0,
ModifiedScoreAscending = 1,
ModifiedScoreTarget = 2,
NotesMissed = 3,
NotesMissedAscending = 4,
NotesMissedTarget = 5,
BadCuts = 6,
BadCutsAscending = 7,
BadCutsTarget = 8,
MaxCombo = 9,
MaxComboAscending = 10,
MaxComboTarget = 11,
GoodCuts = 12,
GoodCutsAscending = 13,
GoodCutsTarget = 14
}
export interface CoreServer {
name: string;
address: string;
port: number;
websocketPort: number;
}
```
Note that you will most probably never use these or refer to this, as these are in the types when you fetch a tournament and are in your intellisense most probably.
For now this is where I will leave the tournament model. Just be aware of what a tournament looks like and what options you have. More options exist and may come in the future, however they are meant to be read only by the server, hence they are not passed on to the client.

513
src/routes/+layout.svelte Normal file
View file

@ -0,0 +1,513 @@
<script lang="ts">
import { onMount } from 'svelte';
import { page } from '$app/stores';
import { goto } from '$app/navigation';
import { discordDataStore, discordTokenStore, authTokenStore } from '$lib/stores';
import { slide } from 'svelte/transition';
let isAuthenticated = false;
let profileDropdownOpen = false;
let userProfile: { name: string; avatar?: string } | null = null;
onMount(() => {
// Check authentication on mount
isAuthenticated = !!$authTokenStore && !!$discordDataStore;
if (isAuthenticated) {
userProfile = {
name: $discordDataStore.global_name,
avatar: `https://cdn.discordapp.com/avatars/${$discordDataStore.id}/${$discordDataStore.avatar}.png`
};
}
});
function toggleProfileDropdown() {
profileDropdownOpen = !profileDropdownOpen;
}
function logout() {
authTokenStore.set(null);
discordDataStore.set(null);
discordTokenStore.set(null);
isAuthenticated = false;
userProfile = null;
profileDropdownOpen = false;
goto('/');
}
function handleClickOutside(event: MouseEvent) {
const dropdown = document.querySelector('.profile-container');
if (dropdown && !dropdown.contains(event.target as Node) && profileDropdownOpen) {
profileDropdownOpen = false;
}
}
</script>
<svelte:window on:click={handleClickOutside} />
<div class="layout dark-mode">
<header>
<div class="navbar">
<div class="navbar-section logo-section">
<img class="logo-svg" src="/assets/LogoTextC_DevW.svg" alt="">
</div>
<nav class="navbar-section nav-links">
<a href="/" class:active={$page.url.pathname === '/'}>
<span>Home</span>
<div class="hover-indicator"></div>
</a>
<a href="/tournaments" class:active={$page.url.pathname === '/tournaments'}>
<span>View Tournaments</span>
<div class="hover-indicator"></div>
</a>
<a href="/authTokens" class:active={$page.url.pathname === '/authTokens'}>
<span>My Tokens</span>
<div class="hover-indicator"></div>
</a>
</nav>
<div class="navbar-section profile-container">
<button class="profile-button" on:click={toggleProfileDropdown}>
{#if isAuthenticated && userProfile?.avatar}
<img src={userProfile.avatar} alt="Profile" class="profile-image" />
{:else}
<span class="material-icons">account_circle</span>
<span class="login-text">{isAuthenticated ? userProfile?.name : 'Login'}</span>
{/if}
</button>
{#if profileDropdownOpen}
<div class="profile-dropdown" transition:slide={{duration: 200}}>
{#if isAuthenticated}
<a href="/authTokens" class="dropdown-item">
<span class="material-icons">vpn_key</span>
<span>Manage Auth (MAT)</span>
</a>
<div class="divider"></div>
<button class="dropdown-item logout" on:click={logout}>
<span class="material-icons">exit_to_app</span>
<span>Logout</span>
</button>
{:else}
<a href="/discordAuth" class="dropdown-item">
<span class="material-icons">login</span>
<span>Login</span>
</a>
{/if}
</div>
{/if}
</div>
</div>
</header>
<main>
<div class="content-container">
<slot />
</div>
</main>
<footer>
<div class="footer-content">
<div class="footer-section brand">
<h3>Tournament Assistant</h3>
<p>Helping you manage tournaments with ease</p>
</div>
<div class="footer-section links">
<h4>Links</h4>
<div class="footer-links">
<a href="https://github.com/ServerBP" target="_blank" rel="noopener noreferrer">
<span class="material-icons">code</span>
GitHub
</a>
<a href="http://tournamentassistant.net" target="_blank" rel="noopener noreferrer">
<span class="material-icons">sports_esports</span>
Tournament Assistant
</a>
<a href="/documentation" target="_blank" rel="noopener noreferrer">
<span class="material-icons">menu_book</span>
Documentation
</a>
</div>
</div>
<div class="footer-section contact">
<h4>Contact</h4>
<a href="mailto:support@tournamentassistant.net" class="contact-link">
<span class="material-icons">email</span>
support@tournamentassistant.net
</a>
<a href="https://discord.gg/tournament" class="contact-link">
<span class="material-icons">forum</span>
Join our Discord
</a>
</div>
</div>
<div class="footer-bottom">
<div class="copyright">
&copy; {new Date().getFullYear()} Tournament Assistant. All rights reserved.
</div>
</div>
</footer>
</div>
<style>
/* Dark mode variables */
:root {
--bg-primary: #121212;
--bg-secondary: #1e1e1e;
--bg-tertiary: #2d2d2d;
--text-primary: #ffffff;
--text-secondary: #b3b3b3;
--accent-color: #4f46e5;
--accent-hover: #6366f1;
--accent-glow: rgba(99, 102, 241, 0.5);
--danger-color: #ef4444;
--danger-hover: #dc2626;
--border-color: #333333;
--navbar-height: 4rem;
--footer-bg: #191919;
}
/* Global styles */
.layout {
display: flex;
flex-direction: column;
min-height: 100vh;
background-color: var(--bg-primary);
color: var(--text-primary);
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
}
/* Header and navbar */
header {
background-color: var(--bg-secondary);
border-bottom: 1px solid var(--border-color);
position: sticky;
top: 0;
z-index: 100;
height: var(--navbar-height);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
}
.navbar {
display: grid;
grid-template-columns: 1fr auto 1fr;
align-items: center;
height: 100%;
padding: 0 1.5rem;
max-width: 1200px;
margin: 0 auto;
width: 100%;
}
.navbar-section {
display: flex;
align-items: center;
}
.logo-section {
justify-content: flex-start;
right: 5rem;
padding-right: 10rem;
}
.logo-svg {
height: 40px;
width: auto;
}
/* Navigation links - centered */
.nav-links {
justify-content: center;
gap: 2rem;
height: 100%;
}
.nav-links a {
color: var(--text-secondary);
text-decoration: none;
font-weight: 500;
padding: 0 0.75rem;
height: 100%;
display: flex;
align-items: center;
position: relative;
transition: color 0.2s ease;
}
.nav-links a:hover {
color: var(--text-primary);
}
.nav-links a.active {
color: var(--text-primary);
}
.nav-links a .hover-indicator {
position: absolute;
bottom: 0;
left: 0;
width: 0;
height: 3px;
background: linear-gradient(90deg, transparent, var(--accent-color), transparent);
transition: width 0.3s ease;
opacity: 0;
}
.nav-links a:hover .hover-indicator {
width: 100%;
opacity: 1;
box-shadow: 0 0 8px var(--accent-glow);
}
.nav-links a.active .hover-indicator {
width: 100%;
opacity: 1;
box-shadow: 0 0 12px var(--accent-glow);
background: linear-gradient(90deg, transparent, var(--accent-hover), transparent);
}
/* Profile section */
.profile-container {
position: relative;
justify-content: flex-end;
}
.profile-button {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem 0.75rem;
background-color: transparent;
border: none;
color: var(--text-primary);
cursor: pointer;
border-radius: 0.375rem;
transition: background-color 0.2s ease;
}
.profile-button:hover {
background-color: var(--bg-tertiary);
}
.profile-image {
width: 2.5rem;
height: 2.5rem;
border-radius: 50%;
object-fit: cover;
border: 2px solid var(--accent-color);
}
.material-icons {
font-size: 1.5rem;
}
.login-text {
font-weight: 500;
}
/* Profile dropdown */
.profile-dropdown {
position: absolute;
top: calc(100% + 0.5rem);
right: 0;
background-color: var(--bg-secondary);
border: 1px solid var(--border-color);
border-radius: 0.75rem;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
width: 220px;
z-index: 10;
overflow: hidden;
}
.dropdown-item {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 0.75rem 1rem;
color: var(--text-primary);
text-decoration: none;
transition: background-color 0.2s ease;
width: 100%;
text-align: left;
border: none;
background: none;
font-size: 0.875rem;
cursor: pointer;
}
.dropdown-item:hover {
background-color: var(--bg-tertiary);
}
.dropdown-item.logout {
color: var(--danger-color);
}
.dropdown-item.logout:hover {
background-color: rgba(239, 68, 68, 0.1);
}
.divider {
height: 1px;
background-color: var(--border-color);
margin: 0.5rem 0;
}
/* Main content */
main {
flex: 1;
padding: 2rem 1.5rem;
}
.content-container {
max-width: 1200px;
margin: 0 auto;
width: 100%;
}
/* Footer */
footer {
background-color: var(--footer-bg);
border-top: 1px solid var(--border-color);
margin-top: auto;
padding-top: 2.5rem;
}
.footer-content {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
gap: 2rem;
max-width: 1200px;
margin: 0 auto;
width: 100%;
padding: 0 1.5rem 2.5rem;
}
.footer-section h3 {
font-size: 1.25rem;
margin-bottom: 0.75rem;
font-weight: 600;
}
.footer-section h4 {
font-size: 1rem;
margin-bottom: 1rem;
color: var(--accent-color);
font-weight: 600;
}
.footer-section p {
color: var(--text-secondary);
font-size: 0.875rem;
margin-bottom: 1rem;
line-height: 1.5;
}
.footer-links {
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.footer-links a, .contact-link {
display: flex;
align-items: center;
gap: 0.5rem;
color: var(--text-secondary);
text-decoration: none;
transition: color 0.2s ease;
font-size: 0.875rem;
}
.footer-links a:hover, .contact-link:hover {
color: var(--text-primary);
}
.footer-links .material-icons, .contact-link .material-icons {
font-size: 1.125rem;
}
.contact-link {
margin-bottom: 0.75rem;
}
.footer-bottom {
background-color: rgba(0, 0, 0, 0.2);
padding: 1rem 0;
text-align: center;
}
.copyright {
color: var(--text-secondary);
font-size: 0.75rem;
}
/* Media queries */
@media (max-width: 768px) {
.navbar {
display: flex;
flex-direction: column;
gap: 1rem;
padding: 1rem;
height: auto;
}
.navbar-section {
width: 100%;
justify-content: center;
}
.logo-section {
padding-right: 0;
}
.nav-links {
width: 100%;
height: auto;
gap: 1rem;
}
.nav-links a {
height: auto;
padding: 0.5rem 0.75rem;
}
.nav-links a .hover-indicator {
bottom: -4px;
}
.profile-container {
width: 100%;
justify-content: center;
}
.profile-dropdown {
position: absolute;
right: auto;
}
.footer-content {
grid-template-columns: 1fr;
text-align: center;
}
.footer-links, .contact-link {
align-items: center;
justify-content: center;
}
}
@media (min-width: 769px) and (max-width: 1024px) {
.footer-content {
grid-template-columns: 1fr 1fr;
}
.footer-section.brand {
grid-column: span 2;
text-align: center;
}
}
</style>

5
src/routes/+layout.ts Normal file
View file

@ -0,0 +1,5 @@
// Tauri doesn't have a Node.js server to do proper SSR
// so we will use adapter-static to prerender the app (SSG)
// See: https://beta.tauri.app/start/frontend/sveltekit/ for more info
export const prerender = false;
export const ssr = false;

270
src/routes/+page.svelte Normal file
View file

@ -0,0 +1,270 @@
<script lang="ts">
import { onMount } from 'svelte';
import { fade } from 'svelte/transition';
let text = '';
let targetText = 'taui.shyyluna.dev';
let currentIndex = 0;
let typingComplete = false;
let cursorVisible = true;
// Typing effect
function typeEffect() {
if (currentIndex < targetText.length) {
text = targetText.substring(0, currentIndex + 1);
currentIndex++;
// Randomize typing speed between 50ms and 150ms for a more realistic effect
const randomSpeed = Math.floor(Math.random() * 100) + 50;
setTimeout(typeEffect, randomSpeed);
} else {
typingComplete = true;
// Continue cursor blinking after typing is complete
setInterval(() => {
cursorVisible = !cursorVisible;
}, 530);
}
}
// Start cursor blinking before typing begins
onMount(() => {
setInterval(() => {
if (!typingComplete) {
cursorVisible = !cursorVisible;
}
}, 530);
// Start typing after a small delay
setTimeout(typeEffect, 1000);
});
</script>
<div class="typing-container">
<span class="typed-text">{text}</span>
<span class="cursor" class:blink={cursorVisible}>|</span>
</div>
<main>
<div class="hero-section">
{#if typingComplete}
<div class="content" transition:fade={{ duration: 800 }}>
<h1>Tournament Assistant UI</h1>
<p>Streamline your tournament management with our powerful and intuitive platform</p>
<a href="/tournaments" class="cta-button">Use TAUI</a>
</div>
{/if}
</div>
<div class="features-section">
<div class="feature-card">
<div class="feature-icon">
<i class="material-icons">speed</i>
</div>
<h3>Fast & Efficient</h3>
<p>Organise tournaments with maximum efficiency and minimal effort. We aim to make it as easy as possible ;)</p>
</div>
<div class="feature-card">
<div class="feature-icon">
<i class="material-icons">devices</i>
</div>
<h3>Fully Responsive</h3>
<p>Hopefully works seamlessly across all devices, from desktop to mobile. (Although I am very much working...)</p>
</div>
<div class="feature-card">
<div class="feature-icon">
<i class="material-icons">code</i>
</div>
<h3>Build More</h3>
<p>Use moons-ta-client to build what you need! Overlays, casting panels, think of anything!</p>
</div>
</div>
</main>
<style>
/* Typing effect styles */
.typing-container {
font-size: 1.5rem;
font-weight: 700;
letter-spacing: 1px;
display: flex;
align-items: center;
margin: 2rem 0;
justify-content: center;
}
.typed-text {
color: var(--accent-color);
text-shadow: 0 0 10px var(--accent-glow);
}
.cursor {
color: var(--accent-color);
margin-left: 2px;
font-weight: 700;
text-shadow: 0 0 10px var(--accent-glow);
}
.cursor.blink {
opacity: 1;
}
.cursor:not(.blink) {
opacity: 0;
}
/* Main content */
main {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
padding: 2rem 1.5rem;
max-width: 1200px;
margin: 0 auto;
width: 100%;
}
/* Hero section */
.hero-section {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
padding: 4rem 0;
width: 100%;
}
.content {
max-width: 800px;
}
.hero-section h1 {
font-size: 3.5rem;
margin-bottom: 1.5rem;
background: linear-gradient(90deg, var(--accent-color), var(--accent-hover));
-webkit-background-clip: text;
background-clip: text;
color: transparent;
text-shadow: 0 0 15px var(--accent-glow);
}
.hero-section p {
font-size: 1.25rem;
color: var(--text-secondary);
margin-bottom: 3rem;
line-height: 1.6;
}
/* CTA button - glowing on hover with no fill or border */
.cta-button {
display: inline-block;
font-size: 1.25rem;
font-weight: 600;
color: var(--text-primary);
text-decoration: none;
padding: 1rem 2.5rem;
border-radius: 4px;
background-color: transparent;
position: relative;
transition: all 0.3s ease;
letter-spacing: 1.5px;
overflow: hidden;
}
.cta-button::after {
content: '';
position: absolute;
bottom: 0;
left: 0;
width: 100%;
height: 3px;
background: linear-gradient(90deg, transparent, var(--accent-color), transparent);
transform: translateX(-100%);
transition: transform 0.3s ease;
opacity: 0;
}
.cta-button:hover {
color: var(--accent-hover);
text-shadow: 0 0 8px var(--accent-glow);
}
.cta-button:hover::after {
transform: translateX(0);
opacity: 1;
box-shadow: 0 0 15px var(--accent-glow);
}
/* Features section */
.features-section {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 2rem;
width: 100%;
margin-top: 3rem;
}
.feature-card {
background-color: var(--bg-secondary);
border-radius: 8px;
padding: 2rem;
text-align: center;
transition: transform 0.3s ease, box-shadow 0.3s ease;
border: 1px solid var(--border-color);
}
.feature-card:hover {
transform: translateY(-5px);
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.3);
}
.feature-icon {
display: flex;
justify-content: center;
align-items: center;
margin-bottom: 1.5rem;
}
.feature-icon i {
font-size: 3rem;
color: var(--accent-color);
}
.feature-card h3 {
font-size: 1.5rem;
margin-bottom: 1rem;
}
.feature-card p {
color: var(--text-secondary);
line-height: 1.6;
}
/* Responsive design */
@media (max-width: 768px) {
.hero-section h1 {
font-size: 2.5rem;
}
.hero-section p {
font-size: 1rem;
}
.typing-container {
font-size: 1.2rem;
}
}
@media (max-width: 480px) {
.hero-section h1 {
font-size: 2rem;
}
.features-section {
grid-template-columns: 1fr;
}
}
</style>

View file

@ -0,0 +1,40 @@
import { error } from '@sveltejs/kit';
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
// Get the directory path
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
// Path to markdown files
const DOCS_PATH = path.resolve(__dirname, '../../../../lib/taDocs');
export async function GET({ params }) {
const { slug } = params;
// Validate filename to prevent directory traversal attacks
if (!slug || slug.includes('..') || slug.includes('/') || !slug.endsWith('.md')) {
throw error(400, 'Invalid documentation file requested');
}
const filePath = path.join(DOCS_PATH, slug);
try {
if (!fs.existsSync(filePath)) {
throw error(404, 'Documentation file not found');
}
const content = fs.readFileSync(filePath, 'utf-8');
return new Response(content, {
headers: {
'Content-Type': 'text/markdown',
'Cache-Control': 'max-age=600' // Cache for 10 minutes
}
});
} catch (err) {
console.error(`Error reading documentation file: ${filePath}`, err);
throw error(500, 'Error loading documentation');
}
}

View file

@ -0,0 +1,924 @@
<script lang="ts">
import { onMount, onDestroy } from 'svelte';
import Notification from '$lib/components/notifications/Popup.svelte';
import { discordAuthUrl } from '$lib/config.json';
import { discordDataStore, discordTokenStore, authTokenStore, TAServerPort, TAServerUrl } from '$lib/stores';
import { TAClient, Response_ResponseType } from 'moons-ta-client';
import { v4 as uuidv4 } from "uuid";
let showLoginNotification: boolean = false;
let showCreateTokenModal: boolean = false;
let showTokenCreatedModal: boolean = false;
let error: string = "";
let isLoggedIn: boolean = false;
let loading: boolean = true;
let authTokens: any[] = [];
let newBotName: string = "";
let newlyCreatedToken: string = "";
let creatingToken: boolean = false;
// Change to an object for better reactivity
let copyClicked: Record<string, boolean> = {};
// Declare client at component level so we can access it in onDestroy
let client: TAClient = new TAClient();
// Remove 'Bearer ' from the token
if ($authTokenStore) {
const cleanToken = $authTokenStore.replace('Bearer ', '');
client.setAuthToken(cleanToken);
}
onMount(async () => {
try {
// Check if user is logged in
isLoggedIn = !!$authTokenStore && !!$discordDataStore;
if (!isLoggedIn) {
showLoginNotification = true;
loading = false;
return;
}
try {
const connectResult = await client.connect($TAServerUrl, $TAServerPort);
if (connectResult.details.oneofKind === "connect" && connectResult.type === Response_ResponseType.Fail) {
error = "Failed to connect to server";
loading = false;
return;
}
const tokens = await client.getBotTokensForUser($discordDataStore.id);
authTokens = (tokens as any).details.getBotTokensForUser?.botUsers || [];
loading = false;
} catch (connErr) {
console.error('TAClient connection error:', connErr);
error = "Connection error";
loading = false;
}
} catch (e) {
console.error('Failed to fetch auth tokens', e);
error = "Failed to fetch auth tokens";
loading = false;
}
});
onDestroy(() => {
if (client) {
// Properly disconnect and clean up any listeners
client.disconnect();
}
});
function closeNotification() {
showLoginNotification = false;
}
function copyToClipboard(text: string) {
// Set the copy status to true in the object
copyClicked = { ...copyClicked, [text]: true };
navigator.clipboard.writeText(text)
.then(() => {
console.log('Copied to clipboard!');
})
.catch(err => {
console.error('Failed to copy: ', err);
});
// Reset after timeout
setTimeout(() => {
copyClicked = { ...copyClicked, [text]: false };
}, 3000);
}
function openCreateTokenModal() {
newBotName = "";
showCreateTokenModal = true;
}
function closeCreateTokenModal() {
showCreateTokenModal = false;
}
function closeTokenCreatedModal() {
showTokenCreatedModal = false;
}
async function createBotToken() {
if (!newBotName.trim()) return;
creatingToken = true;
try {
const createTokenRes = await client.generateBotToken(newBotName.trim());
// Set the new bot token
newlyCreatedToken = (createTokenRes as any).details.generateBotToken?.botToken;
// Refresh token list
const tokens = await client.getBotTokensForUser($discordDataStore.id);
authTokens = (tokens as any).details.getBotTokensForUser?.botUsers || [];
// Close create modal and show the created token modal
closeCreateTokenModal();
showTokenCreatedModal = true;
} catch (err) {
console.error('Failed to create bot token:', err);
error = "Failed to create bot token";
} finally {
creatingToken = false;
}
}
async function deleteBotToken(tokenId: string) {
try {
await client.revokeBotToken(tokenId);
// Refresh the list after deletion
const tokens = await client.getBotTokensForUser($discordDataStore.id);
authTokens = (tokens as any).details.getBotTokensForUser?.botUsers || [];
} catch (err) {
console.error('Failed to delete bot token:', err);
error = "Failed to delete bot token";
}
}
</script>
<main>
<section class="hero-section">
<div class="content">
<h1>Bot Tokens</h1>
<p>Manage your tokens for API and NPM client access</p>
</div>
</section>
{#if error}
<div class="error-message">
<i class="material-icons">error</i>
<p>{error}</p>
</div>
{/if}
<section class="tokens-section">
<div class="section-header">
<div class="spacer"></div>
{#if isLoggedIn && !error}
<button class="create-token-btn" on:click={openCreateTokenModal}>
<i class="material-icons">add</i>
<span>Create Bot Token</span>
</button>
{/if}
</div>
{#if loading}
<div class="loading-container">
<div class="loading-spinner"></div>
<p>Loading tokens...</p>
</div>
{:else if error}
<div class="error-container">
<div class="error-icon">
<i class="material-icons">error_outline</i>
</div>
<p>Failed to load tokens. Please try again later.</p>
<button class="retry-button" on:click={() => window.location.reload()}>
<i class="material-icons">refresh</i>
Retry
</button>
</div>
{:else if authTokens.length === 0}
<div class="empty-container">
<div class="empty-icon">
<i class="material-icons">vpn_key</i>
</div>
<p>No bot tokens found.</p>
{#if isLoggedIn}
<button class="create-first-token-btn" on:click={openCreateTokenModal}>
<i class="material-icons">add_circle</i>
Create Your First Bot Token
</button>
{/if}
</div>
{:else}
<div class="tokens-table-container">
<table class="tokens-table">
<thead>
<tr>
<th>Name</th>
<th>Token ID</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{#each authTokens as token}
<tr>
<td class="token-name">{token.username}</td>
<td class="token-guid">
<div class="guid-container">
<span class="guid-text">{token.guid}</span>
<button class="copy-button" on:click={() => copyToClipboard(token.guid)}>
{#if copyClicked[token.guid]}
<i class="material-icons">check_circle</i>
{:else}
<i class="material-icons">content_copy</i>
{/if}
</button>
</div>
</td>
<td class="token-actions">
<button class="delete-button" on:click={() => deleteBotToken(token.guid)}>
<i class="material-icons">delete</i>
</button>
</td>
</tr>
{/each}
</tbody>
</table>
</div>
{/if}
</section>
<!-- Create Token Modal -->
{#if showCreateTokenModal}
<!-- svelte-ignore a11y-no-static-element-interactions -->
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div class="modal-backdrop" on:click={closeCreateTokenModal}></div>
<div class="modal-container">
<!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div class="modal-content" on:click|stopPropagation>
<div class="modal-header">
<h2>Create Bot Token</h2>
<button class="close-btn" on:click={closeCreateTokenModal}>
<i class="material-icons">close</i>
</button>
</div>
<div class="modal-body">
<div class="form-group">
<label for="botName">Bot Name</label>
<input
type="text"
id="botName"
bind:value={newBotName}
placeholder="Enter a name for your bot"
/>
</div>
<p class="token-warning">
<i class="material-icons">warning</i>
After creation, the token will be shown only once. Please save it in a secure location.
</p>
</div>
<div class="modal-footer">
<button class="cancel-btn" on:click={closeCreateTokenModal}>Cancel</button>
<button
class="create-btn"
on:click={createBotToken}
disabled={!newBotName.trim() || creatingToken}
>
{#if creatingToken}
<span class="btn-spinner"></span>
{:else}
<i class="material-icons">vpn_key</i>
{/if}
Create Token
</button>
</div>
</div>
</div>
{/if}
<!-- Token Created Modal -->
<!-- svelte-ignore a11y-no-static-element-interactions -->
{#if showTokenCreatedModal}
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div class="modal-backdrop" on:click={closeTokenCreatedModal}></div>
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div class="modal-container">
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div class="modal-content" on:click|stopPropagation>
<div class="modal-header">
<h2>Bot Token Created</h2>
<button class="close-btn" on:click={closeTokenCreatedModal}>
<i class="material-icons">close</i>
</button>
</div>
<div class="modal-body">
<div class="token-created-message">
<i class="material-icons success-icon">check_circle</i>
<p>Your bot token has been created successfully!</p>
</div>
<div class="form-group">
<!-- svelte-ignore a11y-label-has-associated-control -->
<label>Token (copy this now, it won't be shown again and cannot be regenerated)</label>
<div class="token-display">
<input
type="text"
readonly
value={newlyCreatedToken}
class="token-input"
/>
<button class="copy-token-btn" on:click={() => copyToClipboard(newlyCreatedToken)}>
{#if copyClicked[newlyCreatedToken]}
<i class="material-icons">check_circle</i>
{:else}
<i class="material-icons">content_copy</i>
{/if}
</button>
</div>
</div>
</div>
<div class="modal-footer">
<button class="create-btn" on:click={closeTokenCreatedModal}>
<i class="material-icons">check</i>
Done
</button>
</div>
</div>
</div>
{/if}
<Notification
bind:open={showLoginNotification}
message="You need to be logged in with Discord to manage bot tokens"
icon="login"
iconColor="primary"
buttons={[
{
text: "Login with Discord",
type: "primary",
href: discordAuthUrl,
icon: "discord",
color: "primary"
},
{
text: "Back to Home",
type: "outlined",
href: "/",
icon: "home",
color: "secondary"
}
]}
on:close={closeNotification}
/>
</main>
<style>
/* Main content */
main {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
padding: 2rem 1.5rem;
max-width: 1200px;
margin: 0 auto;
width: 100%;
}
/* Hero section */
.hero-section {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
padding: 1rem 0;
width: 100%;
}
.content {
max-width: 800px;
}
.hero-section h1 {
font-size: 3.5rem;
background: linear-gradient(90deg, var(--accent-color), var(--accent-hover));
-webkit-background-clip: text;
background-clip: text;
color: transparent;
text-shadow: 0 0 15px var(--accent-glow);
}
.hero-section p {
font-size: 1.25rem;
color: var(--text-secondary);
margin-bottom: 2rem;
line-height: 1.6;
}
/* Section header with Create Token button */
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
margin-bottom: 1.5rem;
}
.spacer {
flex: 1;
}
.create-token-btn {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.75rem 1.25rem;
background: linear-gradient(135deg, var(--accent-color), var(--accent-hover));
color: white;
border: none;
border-radius: 8px;
font-weight: 600;
font-size: 1rem;
cursor: pointer;
transition: all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
}
.create-token-btn:hover {
transform: translateY(-2px);
box-shadow: 0 6px 15px rgba(0, 0, 0, 0.2), 0 0 10px var(--accent-glow);
}
.create-token-btn:active {
transform: translateY(0);
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
}
.create-token-btn i {
font-size: 1.25rem;
}
.create-first-token-btn {
display: flex;
align-items: center;
gap: 0.5rem;
margin-top: 1.5rem;
padding: 0.75rem 1.5rem;
background: linear-gradient(135deg, var(--accent-color), var(--accent-hover));
color: white;
border: none;
border-radius: 8px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
}
.create-first-token-btn:hover {
transform: translateY(-2px);
box-shadow: 0 6px 15px rgba(0, 0, 0, 0.2), 0 0 10px var(--accent-glow);
}
/* Error message */
.error-message {
display: flex;
align-items: center;
background-color: rgba(255, 0, 0, 0.1);
border-left: 4px solid #ff5555;
padding: 1rem;
border-radius: 4px;
margin-bottom: 2rem;
width: 100%;
}
.error-message i {
color: #ff5555;
margin-right: 0.75rem;
}
/* Tokens section */
.tokens-section {
width: 100%;
margin-top: 2rem;
}
/* Tokens table */
.tokens-table-container {
width: 100%;
overflow-x: auto;
background-color: var(--bg-secondary);
border-radius: 12px;
border: 1px solid var(--border-color);
}
.tokens-table {
width: 100%;
border-collapse: collapse;
}
.tokens-table th {
text-align: left;
padding: 1rem;
font-weight: 600;
color: var(--text-secondary);
border-bottom: 1px solid var(--border-color);
}
.tokens-table td {
padding: 1rem;
border-bottom: 1px solid var(--border-color);
}
.tokens-table tr:last-child td {
border-bottom: none;
}
.token-name {
font-weight: 500;
}
.token-guid {
font-family: monospace;
color: var(--text-secondary);
}
.guid-container {
display: flex;
align-items: center;
}
.guid-text {
max-width: 3000px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.copy-button {
background: none;
border: none;
cursor: pointer;
color: var(--text-secondary);
padding: 0;
margin-left: 0.5rem;
display: flex;
align-items: center;
justify-content: center;
opacity: 0.5;
transition: opacity 0.2s;
}
.copy-button:hover {
opacity: 1;
}
.copy-button i {
font-size: 0.875rem;
}
.token-actions {
text-align: right;
}
.delete-button {
background: none;
border: none;
cursor: pointer;
color: #ff5555;
padding: 0.5rem;
border-radius: 4px;
transition: all 0.2s;
display: flex;
align-items: center;
justify-content: center;
}
.delete-button:hover {
background-color: rgba(255, 85, 85, 0.1);
}
/* Loading, error, and empty states */
.loading-container,
.error-container,
.empty-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 4rem 2rem;
text-align: center;
width: 100%;
}
.loading-spinner {
width: 50px;
height: 50px;
border: 4px solid rgba(var(--accent-color-rgb), 0.2);
border-radius: 50%;
border-top-color: var(--accent-color);
animation: spin 1s linear infinite;
margin-bottom: 1.5rem;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.error-icon,
.empty-icon {
font-size: 3rem;
color: var(--accent-color);
margin-bottom: 1.5rem;
}
.error-icon i,
.empty-icon i {
font-size: 3.5rem;
}
.retry-button {
display: flex;
align-items: center;
gap: 0.5rem;
margin-top: 1.5rem;
padding: 0.75rem 1.5rem;
background-color: var(--accent-color);
color: white;
border: none;
border-radius: 4px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
}
.retry-button:hover {
background-color: var(--accent-hover);
box-shadow: 0 0 15px var(--accent-glow);
}
/* Modal styles */
.modal-backdrop {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.7);
backdrop-filter: blur(4px);
z-index: 1000;
animation: fadeIn 0.2s ease;
}
.modal-container {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: flex;
align-items: center;
justify-content: center;
z-index: 1001;
padding: 1rem;
}
.modal-content {
background-color: var(--bg-primary);
border-radius: 12px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3);
width: 100%;
max-width: 500px;
max-height: 90vh;
overflow-y: auto;
animation: modalSlideIn 0.3s cubic-bezier(0.25, 0.8, 0.25, 1);
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1.25rem 1.5rem;
border-bottom: 1px solid var(--border-color);
}
.modal-header h2 {
font-size: 1.5rem;
margin: 0;
color: var(--text-primary);
}
.close-btn {
background: none;
border: none;
color: var(--text-secondary);
cursor: pointer;
padding: 0.5rem;
margin: -0.5rem;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s;
}
.close-btn:hover {
background-color: rgba(0, 0, 0, 0.1);
color: var(--text-primary);
}
.modal-body {
padding: 1.5rem;
}
.form-group {
margin-bottom: 1.5rem;
}
.form-group label {
display: block;
margin-bottom: 0.5rem;
font-weight: 500;
color: var(--text-secondary);
}
.form-group input[type="text"] {
width: 85%;
padding: 0.75rem 1rem;
border: 1px solid var(--border-color);
border-radius: 8px;
background-color: var(--bg-secondary);
color: var(--text-primary);
font-size: 1rem;
transition: all 0.2s;
}
.form-group input[type="text"]:focus {
border-color: var(--accent-color);
box-shadow: 0 0 0 2px rgba(var(--accent-color-rgb), 0.2);
outline: none;
}
.token-warning {
display: flex;
align-items: flex-start;
gap: 0.5rem;
padding: 1rem;
background-color: rgba(255, 193, 7, 0.1);
border-left: 4px solid #ffc107;
border-radius: 4px;
color: var(--text-secondary);
font-size: 0.875rem;
}
.token-warning i {
color: #ffc107;
font-size: 1.25rem;
}
.modal-footer {
display: flex;
justify-content: flex-end;
padding: 1.25rem 1.5rem;
border-top: 1px solid var(--border-color);
gap: 1rem;
}
.cancel-btn {
padding: 0.75rem 1.25rem;
background-color: transparent;
border: 1px solid var(--border-color);
color: var(--text-secondary);
border-radius: 8px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
}
.cancel-btn:hover {
background-color: var(--bg-secondary);
color: var(--text-primary);
}
.create-btn {
padding: 0.75rem 1.5rem;
background: linear-gradient(135deg, var(--accent-color), var(--accent-hover));
color: white;
border: none;
border-radius: 8px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s;
display: flex;
align-items: center;
gap: 0.5rem;
}
.create-btn:hover:not(:disabled) {
box-shadow: 0 0 15px var(--accent-glow);
transform: translateY(-2px);
}
.create-btn:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.btn-spinner {
width: 1rem;
height: 1rem;
border: 2px solid rgba(255, 255, 255, 0.3);
border-radius: 50%;
border-top-color: white;
animation: spin 1s linear infinite;
}
/* Token Created Modal specific styles */
.token-created-message {
display: flex;
align-items: center;
gap: 0.75rem;
margin-bottom: 1.5rem;
}
.success-icon {
color: #4caf50;
font-size: 1.5rem;
}
.token-display {
position: relative;
}
.token-input {
width: 100%;
padding: 0.75rem 1rem;
border: 1px solid var(--border-color);
border-radius: 8px;
background-color: var(--bg-secondary);
color: var(--text-primary);
font-family: monospace;
font-size: 0.875rem;
padding-right: 3rem;
}
.copy-token-btn {
position: absolute;
right: 0.5rem;
top: 50%;
transform: translateY(-50%);
background: none;
border: none;
color: var(--accent-color);
cursor: pointer;
padding: 0.5rem;
border-radius: 4px;
transition: all 0.2s;
}
.copy-token-btn:hover {
background-color: rgba(var(--accent-color-rgb), 0.1);
}
/* Animations */
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
@keyframes modalSlideIn {
from {
opacity: 0;
transform: translateY(-30px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* Responsive design */
@media (max-width: 768px) {
.hero-section h1 {
font-size: 2.5rem;
}
.hero-section p {
font-size: 1rem;
}
.guid-text {
max-width: 120px;
}
}
@media (max-width: 480px) {
.hero-section h1 {
font-size: 2rem;
}
.tokens-table th,
.tokens-table td {
padding: 0.75rem;
}
.guid-text {
max-width: 80px;
}
.create-token-btn span {
display: none;
}
}
</style>

View file

@ -0,0 +1,428 @@
<script lang="ts">
import { onMount } from 'svelte';
import { fade, fly } from 'svelte/transition';
import { discordDataStore, discordTokenStore, authTokenStore } from '$lib/stores';
import { discordAuthUrl, bkAPIUrl } from '$lib/config.json';
let isLoggedIn = false;
let isNewLogin = false;
let isLoading = true;
let userAvatar = '';
let username = '';
let userId = '';
let animationComplete = false;
let authError = '';
let availablePanels: any[] = [];
type PanelDefinition = {
name: string;
route: string;
description: string;
icon: string;
};
const defaultPanels: PanelDefinition[] = [
{
name: "View Tournaments",
route: "/tournaments",
description: "Browse ongoing and upcoming tournaments",
icon: "public"
},
{
name: "My Tokens",
route: "/authTokens",
description: "View your current bot tokens",
icon: "token"
}
];
async function validateAuthToken() {
try {
const response = await fetch(`${bkAPIUrl}/users/@me`, {
headers: {
authorization: $authTokenStore,
}
});
if (response.ok) {
const data = await response.json();
return true;
}
return false;
} catch (error) {
console.error('Error validating auth token:', error);
return false;
}
}
async function handleNewLogin(tokenType: string, accessToken: string) {
try {
discordTokenStore.set(`${tokenType} ${accessToken}`);
const response = await fetch(`${bkAPIUrl}/requestAuthToken`, {
headers: {
authorization: $discordTokenStore,
},
});
const data = await response.json();
if (data.token) {
authTokenStore.set(`Bearer ${data.token}`);
discordDataStore.set(data.discordData);
userId = data.discordData.id;
username = data.discordData.username;
userAvatar = `https://cdn.discordapp.com/avatars/${data.discordData.id}/${data.discordData.avatar}.png`;
isLoggedIn = true;
isNewLogin = true;
// Clean the URL
window.history.replaceState({}, document.title, window.location.pathname);
availablePanels = defaultPanels;
// Set timer for animation completion
setTimeout(() => {
animationComplete = true;
}, 3000);
} else {
authError = 'Failed to retrieve authentication token. Please try again.';
}
} catch (error) {
console.error('Authentication error:', error);
authError = 'An error occurred during authentication. Please try again.';
isLoggedIn = false;
} finally {
isLoading = false;
}
}
onMount(async () => {
// Check if token parameters are in URL (new login)
const hash = window.location.hash.substring(1);
const params = new URLSearchParams(hash);
const tokenType = params.get('token_type');
const accessToken = params.get('access_token');
if (tokenType && accessToken) {
await handleNewLogin(tokenType, accessToken);
} else {
// Check if user is already logged in
if ($authTokenStore && $discordDataStore) {
const isValid = await validateAuthToken();
if (isValid) {
isLoggedIn = true;
username = $discordDataStore.global_name;
userId = $discordDataStore.id;
userAvatar = `https://cdn.discordapp.com/avatars/${$discordDataStore.id}/${$discordDataStore.avatar}.png`;
availablePanels = defaultPanels;
} else {
// Token invalid, clear stores
authTokenStore.set(null);
discordDataStore.set(null);
discordTokenStore.set(null);
}
}
isLoading = false;
}
});
</script>
<main>
{#if isLoading}
<div class="loading-container">
<div class="loading-spinner"></div>
<p>Authenticating...</p>
</div>
{:else if !isLoggedIn}
<div class="auth-container" in:fade={{ duration: 800 }}>
<h1>Log in with Discord</h1>
<p>Connect your Discord account to access TAUI</p>
{#if authError}
<div class="error-message">
<i class="material-icons">error</i>
<p>{authError}</p>
</div>
{/if}
<a href={discordAuthUrl} class="discord-login-button">
<i class="material-icons">discord</i>
Login with Discord
</a>
</div>
{:else if isNewLogin && !animationComplete}
<div class="welcome-animation">
<div class="avatar-container" in:fade={{ duration: 300 }}>
<img src={userAvatar} alt="User avatar" class="large-avatar" />
<h2 in:fade={{ delay: 500, duration: 500 }}>Welcome, {username}!</h2>
</div>
</div>
{:else}
<div class="dashboard-container" in:fade={{ duration: 800 }}>
<div class="user-info">
<img src={userAvatar} alt="User avatar" class="user-avatar" />
<h2>Welcome, {username}</h2>
</div>
<div class="panels-grid">
{#each availablePanels as panel}
<a href={panel.route} class="panel-card" in:fly={{ y: 20, duration: 300, delay: 200 }}>
<div class="panel-icon">
<i class="material-icons">{panel.icon}</i>
</div>
<h3>{panel.name}</h3>
<p>{panel.description}</p>
</a>
{/each}
</div>
</div>
{/if}
</main>
<style>
main {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 2rem 1.5rem;
max-width: 1200px;
margin: 0 auto;
width: 100%;
min-height: 80vh;
}
/* Loading state */
.loading-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.loading-spinner {
width: 50px;
height: 50px;
border: 4px solid var(--bg-secondary);
border-radius: 50%;
border-top: 4px solid var(--accent-color);
animation: spin 1s linear infinite;
margin-bottom: 1rem;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
/* Login state */
.auth-container {
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
max-width: 500px;
}
.auth-container h1 {
font-size: 3rem;
margin-bottom: 1.5rem;
background: linear-gradient(90deg, var(--accent-color), var(--accent-hover));
-webkit-background-clip: text;
background-clip: text;
color: transparent;
text-shadow: 0 0 15px var(--accent-glow);
}
.auth-container p {
font-size: 1.25rem;
color: var(--text-secondary);
margin-bottom: 3rem;
line-height: 1.6;
}
.discord-login-button {
display: flex;
align-items: center;
justify-content: center;
background-color: #5865F2;
color: white;
padding: 1rem 2rem;
border-radius: 4px;
font-size: 1.25rem;
font-weight: 600;
text-decoration: none;
transition: all 0.3s ease;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
.discord-login-button:hover {
background-color: #4752C4;
transform: translateY(-2px);
box-shadow: 0 6px 10px rgba(0, 0, 0, 0.15);
}
.discord-login-button i {
margin-right: 0.75rem;
}
.error-message {
display: flex;
align-items: center;
background-color: rgba(255, 0, 0, 0.1);
border-left: 4px solid #ff5555;
padding: 1rem;
border-radius: 4px;
margin-bottom: 2rem;
width: 100%;
}
.error-message i {
color: #ff5555;
margin-right: 0.75rem;
}
/* Welcome animation */
.welcome-animation {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.avatar-container {
display: flex;
flex-direction: column;
align-items: center;
animation: avatar-shrink 2.5s forwards ease-in-out;
}
.large-avatar {
width: 200px;
height: 200px;
border-radius: 50%;
border: 4px solid var(--accent-color);
box-shadow: 0 0 20px var(--accent-glow);
}
@keyframes avatar-shrink {
0% {
transform: scale(1);
}
80% {
transform: scale(1);
}
100% {
transform: scale(0.3) translateY(-100px);
}
}
/* Dashboard */
.dashboard-container {
width: 100%;
}
.user-info {
display: flex;
align-items: center;
margin-bottom: 3rem;
}
.user-avatar {
width: 60px;
height: 60px;
border-radius: 50%;
border: 2px solid var(--accent-color);
margin-right: 1rem;
}
.panels-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 2rem;
width: 100%;
}
.panel-card {
background-color: var(--bg-secondary);
border-radius: 8px;
padding: 2rem;
text-align: center;
transition: transform 0.3s ease, box-shadow 0.3s ease;
border: 1px solid var(--border-color);
text-decoration: none;
color: var(--text-primary);
display: flex;
flex-direction: column;
min-height: 200px;
background: linear-gradient(145deg, var(--bg-secondary), var(--bg-tertiary));
}
.panel-card:hover {
transform: translateY(-5px);
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.3);
}
.panel-icon {
display: flex;
justify-content: center;
align-items: center;
margin-bottom: 1.5rem;
}
.panel-icon i {
font-size: 3rem;
color: var(--accent-color);
}
.panel-card h3 {
font-size: 1.5rem;
margin-bottom: 1rem;
}
.panel-card p {
color: var(--text-secondary);
line-height: 1.6;
}
/* Responsive styles */
@media (max-width: 768px) {
.auth-container h1 {
font-size: 2.5rem;
}
.auth-container p {
font-size: 1rem;
}
.large-avatar {
width: 150px;
height: 150px;
}
}
@media (max-width: 480px) {
.auth-container h1 {
font-size: 2rem;
}
.panels-grid {
grid-template-columns: 1fr;
}
}
.panel-card:hover i {
animation: icon-bounce 0.5s ease;
}
@keyframes icon-bounce {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-5px); }
}
</style>

View file

@ -0,0 +1,797 @@
<script lang="ts">
import { onMount } from 'svelte';
import { page } from '$app/stores';
import { fly, fade, slide } from 'svelte/transition';
import { marked } from 'marked';
import 'prismjs';
import 'prismjs/components/prism-typescript';
import 'prismjs/components/prism-javascript';
import 'prismjs/components/prism-css';
import 'prismjs/components/prism-json';
import 'prismjs/components/prism-bash';
import 'prismjs/components/prism-markdown';
// Documentation structure - for now this is left as is until I finish writing the rest of the docs, this is just placeholder for structure and what can be done
let docStructure = [
{
category: "Getting Started",
icon: "rocket_launch",
items: [
{ title: "Introduction", file: "intro", order: 1 },
{ title: "Installation", file: "sample", order: 2 },
{ title: "Client", file: "client", order: 3 }
]
},
{
category: "Core Concepts",
icon: "psychology",
items: [
{ title: "Tournaments", file: "", order: 1 },
{ title: "Matches", file: "", order: 2 },
{ title: "Players", file: "", order: 3 }
]
},
{
category: "API Reference",
icon: "api",
items: [
{ title: "Authentication", file: "", order: 1 },
{ title: "Endpoints", file: "", order: 2 },
{ title: "Response Types", file: "", order: 3 }
]
}
];
// Flatten categories for URL handling
let allDocs = docStructure.flatMap(category =>
category.items.map(item => ({
...item,
category: category.category
}))
);
// State
let currentDoc: string = "";
let activeCategory: string = "";
let markdownContent: string = "";
let htmlContent: string = "";
let isLoading: boolean = true;
let isMobileMenuOpen: boolean = false;
let error: string | null = null;
// Handle hash changes
function handleHashChange() {
const hash = window.location.hash.slice(1); // Remove the # character
if (hash) {
const [category, doc] = hash.split("/");
if (category && doc) {
// Both category and doc are specified
activeCategory = decodeURIComponent(category);
currentDoc = decodeURIComponent(doc);
} else {
// Only category is specified, load first doc in that category
activeCategory = decodeURIComponent(hash);
const categoryData = docStructure.find(c => c.category === activeCategory);
if (categoryData && categoryData.items.length > 0) {
currentDoc = categoryData.items[0].file;
}
}
} else {
// No hash, load first doc of first category
if (docStructure.length > 0) {
activeCategory = docStructure[0].category;
currentDoc = docStructure[0].items[0].file;
}
}
loadMarkdownContent();
}
// Load markdown content
async function loadMarkdownContent() {
isLoading = true;
error = null;
try {
// Fetch using API from lib folder
const response = await fetch(`/api/docs/${currentDoc}.md`);
if (!response.ok) {
throw new Error(`Failed to load documentation: ${response.statusText}`);
}
markdownContent = await response.text();
htmlContent = await marked.parse(markdownContent);
} catch (err) {
console.error("Error loading markdown:", err);
error = "Could not load the requested documentation.";
htmlContent = "";
} finally {
isLoading = false;
}
}
// Toggle category expansion
function toggleCategory(category: string) {
if (activeCategory === category) {
activeCategory = "";
} else {
activeCategory = category;
// Find first doc in category and set it as current
const categoryData = docStructure.find(c => c.category === category);
if (categoryData && categoryData.items.length > 0) {
categoryData.items.sort((a, b) => a.order - b.order);
currentDoc = categoryData.items[0].file;
window.location.hash = `${encodeURIComponent(category)}/${encodeURIComponent(currentDoc)}`;
}
}
}
// Select a specific doc
function selectDoc(category: string, doc: string) {
activeCategory = category;
currentDoc = doc;
window.location.hash = `${encodeURIComponent(category)}/${encodeURIComponent(doc)}`;
// On mobile, close the sidebar after selection
if (window.innerWidth < 768) {
isMobileMenuOpen = false;
}
}
// Toggle mobile menu
function toggleMobileMenu() {
isMobileMenuOpen = !isMobileMenuOpen;
}
function highlightCodeBlocks() {
// Make sure Prism is available
if (typeof window !== 'undefined' && (window as any).Prism) {
// Find all pre code elements and highlight them
const codeBlocks = document.querySelectorAll('pre code');
codeBlocks.forEach((block: Element) => {
// Get the language class if it exists (e.g., language-typescript)
const classes = block.classList;
let language = 'typescript'; // Default language
for (let i = 0; i < classes.length; i++) {
if (classes[i].startsWith('language-')) {
language = classes[i].replace('language-', '');
break;
}
}
// Add the language class if it doesn't exist
if (!block.classList.contains(`language-${language}`)) {
block.classList.add(`language-${language}`);
}
// Make sure the parent pre has the language class too
const pre = block.parentElement;
if (pre && !pre.classList.contains(`language-${language}`)) {
pre.classList.add(`language-${language}`);
}
// Highlight the code block
(window as any).Prism.highlightElement(block);
});
}
}
onMount(() => {
// Initialise content based on URL hash or load default
handleHashChange();
// Listen for hash changes
window.addEventListener('hashchange', handleHashChange);
return () => {
window.removeEventListener('hashchange', handleHashChange);
};
});
$: if (htmlContent && !isLoading) {
setTimeout(() => {
highlightCodeBlocks();
}, 0);
}
</script>
<svelte:head>
<title>Documentation | Tournament Assistant</title>
</svelte:head>
<div class="documentation-container">
<!-- Mobile menu toggle -->
<button class="mobile-menu-toggle" on:click={toggleMobileMenu}>
<span class="material-icons">{isMobileMenuOpen ? 'close' : 'menu'}</span>
<span>Documentation</span>
</button>
<!-- Sidebar -->
<aside class="sidebar" class:open={isMobileMenuOpen}>
<div class="sidebar-header">
<h2>Documentation</h2>
<button class="close-sidebar" on:click={toggleMobileMenu}>
<span class="material-icons">close</span>
</button>
</div>
<nav class="sidebar-nav">
{#each docStructure as category}
<div class="category">
<button
class="category-header"
class:active={activeCategory === category.category}
on:click={() => toggleCategory(category.category)}
>
<div class="category-header-content">
<span class="material-icons">{category.icon}</span>
<span>{category.category}</span>
</div>
<span class="material-icons">
{activeCategory === category.category ? 'expand_less' : 'expand_more'}
</span>
</button>
{#if activeCategory === category.category}
<!-- svelte-ignore missing-declaration -->
<div class="category-items" transition:slide={{duration: 200}}>
{#each category.items.sort((a, b) => a.order - b.order) as item}
<button
class="doc-link"
class:active={currentDoc === item.file}
on:click={() => selectDoc(category.category, item.file)}
>
<span>{item.title}</span>
</button>
{/each}
</div>
{/if}
</div>
{/each}
</nav>
</aside>
<!-- Main content -->
<main class="content">
{#if isLoading}
<div class="loading-container" in:fade={{duration: 200}}>
<div class="loading-spinner"></div>
<p>Loading documentation...</p>
</div>
{:else if error}
<div class="error-container" in:fly={{y: 20, duration: 300}}>
<span class="material-icons">error_outline</span>
<h3>Documentation Not Found</h3>
<p>{error}</p>
<button class="error-action-btn" on:click={handleHashChange}>
<span class="material-icons">refresh</span>
Try Again
</button>
</div>
{:else}
<div class="markdown-container" in:fly={{y: 20, duration: 300}}>
{@html htmlContent}
</div>
{/if}
</main>
</div>
<style>
/* Documentation specific styles */
.documentation-container {
display: flex;
height: calc(100vh - var(--navbar-height));
background-color: var(--bg-primary);
position: relative;
}
/* Sidebar styles */
.sidebar {
width: 280px;
background-color: var(--bg-secondary);
border-right: 1px solid var(--border-color);
height: 100%;
overflow-y: auto;
transition: transform 0.3s ease;
z-index: 5;
}
.sidebar-header {
padding: 1.5rem;
border-bottom: 1px solid var(--border-color);
display: flex;
justify-content: space-between;
align-items: center;
}
.sidebar-header h2 {
margin: 0;
font-size: 1.25rem;
color: var(--accent-color);
}
.close-sidebar {
display: none;
background: none;
border: none;
color: var(--text-secondary);
cursor: pointer;
}
.sidebar-nav {
padding: 1rem 0;
}
.category {
margin-bottom: 0.5rem;
}
.category-header {
width: 100%;
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.75rem 1.5rem;
background: none;
border: none;
color: var(--text-primary);
font-size: 1rem;
font-weight: 500;
cursor: pointer;
transition: background-color 0.2s ease;
text-align: left;
}
.category-header:hover {
background-color: var(--bg-tertiary);
}
.category-header.active {
background: linear-gradient(90deg, var(--accent-color) 0.5%, var(--bg-tertiary) 0.5%);
}
.category-header-content {
display: flex;
align-items: center;
gap: 0.75rem;
}
.category-items {
display: flex;
flex-direction: column;
padding-left: 2.5rem;
}
.doc-link {
padding: 0.5rem 1rem;
margin: 0.125rem 0;
background: none;
border: none;
color: var(--text-secondary);
font-size: 0.875rem;
text-align: left;
cursor: pointer;
border-radius: 0.25rem;
transition: all 0.2s ease;
}
.doc-link:hover {
background-color: var(--bg-tertiary);
color: var(--text-primary);
}
.doc-link.active {
background-color: rgba(79, 70, 229, 0.15);
color: var(--accent-color);
box-shadow: 0 0 8px rgba(79, 70, 229, 0.2);
}
/* Mobile menu toggle */
.mobile-menu-toggle {
display: none;
position: fixed;
top: calc(var(--navbar-height) + 1rem);
left: 1rem;
z-index: 10;
padding: 0.5rem 1rem;
background-color: var(--accent-color);
color: white;
border: none;
border-radius: 0.375rem;
font-weight: 500;
cursor: pointer;
box-shadow: 0 4px 12px rgba(79, 70, 229, 0.3);
align-items: center;
gap: 0.5rem;
}
/* Main content */
.content {
flex: 1;
padding: 2rem;
overflow-y: auto;
max-width: 100%;
}
/* Loading styles */
.loading-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
gap: 1rem;
color: var(--text-secondary);
}
.loading-spinner {
width: 2.5rem;
height: 2.5rem;
border: 0.25rem solid rgba(79, 70, 229, 0.3);
border-top: 0.25rem solid var(--accent-color);
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
/* Error styles */
.error-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 60%;
text-align: center;
padding: 2rem;
gap: 1rem;
color: var(--text-secondary);
}
.error-container .material-icons {
font-size: 3rem;
color: var(--danger-color);
}
.error-container h3 {
margin: 0;
font-size: 1.5rem;
color: var(--text-primary);
}
.error-action-btn {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem 1rem;
background-color: var(--bg-tertiary);
color: var(--text-primary);
border: 1px solid var(--border-color);
border-radius: 0.375rem;
margin-top: 1rem;
cursor: pointer;
transition: all 0.2s ease;
}
.error-action-btn:hover {
background-color: var(--accent-color);
border-color: var(--accent-color);
box-shadow: 0 0 12px var(--accent-glow);
}
/* Markdown content styling */
.markdown-container {
max-width: 800px;
margin: 0 auto;
}
.markdown-container :global(h1) {
font-size: 2rem;
font-weight: 700;
margin-bottom: 1.5rem;
padding-bottom: 0.5rem;
border-bottom: 1px solid var(--border-color);
color: var(--accent-color);
}
.markdown-container :global(h2) {
font-size: 1.5rem;
font-weight: 600;
margin-top: 2rem;
margin-bottom: 1rem;
color: var(--text-primary);
}
.markdown-container :global(h3) {
font-size: 1.25rem;
font-weight: 600;
margin-top: 1.5rem;
margin-bottom: 0.75rem;
color: var(--text-primary);
}
.markdown-container :global(p) {
margin-bottom: 1rem;
line-height: 1.6;
color: var(--text-secondary);
}
.markdown-container :global(ul), .markdown-container :global(ol) {
margin-bottom: 1rem;
padding-left: 1.5rem;
color: var(--text-secondary);
}
.markdown-container :global(li) {
margin-bottom: 0.5rem;
line-height: 1.6;
}
.markdown-container :global(code) {
background-color: var(--bg-tertiary);
padding: 0.2em 0.4em;
border-radius: 0.25rem;
font-family: monospace;
font-size: 0.875em;
}
.markdown-container :global(pre) {
background-color: var(--bg-tertiary);
padding: 1rem;
border-radius: 0.5rem;
overflow-x: auto;
margin-bottom: 1.5rem;
border: 1px solid var(--border-color);
}
.markdown-container :global(pre code) {
background-color: transparent;
padding: 0;
border-radius: 0;
display: block;
}
.markdown-container :global(blockquote) {
border-left: 4px solid var(--accent-color);
padding-left: 1rem;
margin-left: 0;
margin-right: 0;
margin-bottom: 1rem;
color: var(--text-secondary);
background-color: rgba(79, 70, 229, 0.07);
padding: 0.75rem 1rem;
border-radius: 0 0.375rem 0.375rem 0;
}
.markdown-container :global(table) {
width: 100%;
border-collapse: collapse;
margin-bottom: 1.5rem;
}
.markdown-container :global(th), .markdown-container :global(td) {
padding: 0.75rem;
border: 1px solid var(--border-color);
text-align: left;
}
.markdown-container :global(th) {
background-color: var(--bg-tertiary);
font-weight: 600;
}
.markdown-container :global(tr:nth-child(even)) {
background-color: rgba(45, 45, 45, 0.5);
}
.markdown-container :global(a) {
color: var(--accent-color);
text-decoration: none;
border-bottom: 1px dotted var(--accent-color);
transition: border-bottom 0.2s ease;
}
.markdown-container :global(a:hover) {
border-bottom: 1px solid var(--accent-color);
}
.markdown-container :global(hr) {
border: none;
border-top: 1px solid var(--border-color);
margin: 2rem 0;
}
.markdown-container :global(img) {
max-width: 100%;
border-radius: 0.5rem;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
}
/* Custom info box style */
.markdown-container :global(.info-box),
.markdown-container :global(.warning-box),
.markdown-container :global(.success-box) {
border-radius: 0.5rem;
padding: 1rem;
margin-bottom: 1.5rem;
display: flex;
gap: 1rem;
align-items: flex-start;
}
.markdown-container :global(.info-box) {
background-color: rgba(59, 130, 246, 0.1);
border-left: 4px solid #3b82f6;
}
.markdown-container :global(.warning-box) {
background-color: rgba(245, 158, 11, 0.1);
border-left: 4px solid #f59e0b;
}
.markdown-container :global(.success-box) {
background-color: rgba(34, 197, 94, 0.1);
border-left: 4px solid #22c55e;
}
/* Responsive styles */
@media (max-width: 768px) {
.mobile-menu-toggle {
display: flex;
}
.sidebar {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
transform: translateX(-100%);
z-index: 999;
}
.sidebar.open {
transform: translateX(0);
}
.close-sidebar {
display: block;
}
.content {
padding: 4rem 1rem 2rem;
}
}
/* PrismJS VSCode-like Dark Theme */
.markdown-container :global(pre) {
background-color: #1e1e1e;
padding: 1rem;
border-radius: 0.5rem;
overflow-x: auto;
margin-bottom: 1.5rem;
border: 1px solid var(--border-color);
}
.markdown-container :global(code[class*="language-"]),
.markdown-container :global(pre[class*="language-"]) {
color: #d4d4d4;
font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace;
text-align: left;
white-space: pre;
word-spacing: normal;
word-break: normal;
word-wrap: normal;
line-height: 1.5;
tab-size: 4;
hyphens: none;
}
/* Tokens */
.markdown-container :global(.token.comment),
.markdown-container :global(.token.prolog),
.markdown-container :global(.token.doctype),
.markdown-container :global(.token.cdata) {
color: #6a9955;
}
.markdown-container :global(.token.punctuation) {
color: #d4d4d4;
}
.markdown-container :global(.token.namespace) {
opacity: 0.7;
}
.markdown-container :global(.token.property),
.markdown-container :global(.token.tag),
.markdown-container :global(.token.boolean),
.markdown-container :global(.token.number),
.markdown-container :global(.token.constant),
.markdown-container :global(.token.symbol) {
color: #b5cea8;
}
.markdown-container :global(.token.selector),
.markdown-container :global(.token.attr-name),
.markdown-container :global(.token.string),
.markdown-container :global(.token.char),
.markdown-container :global(.token.builtin) {
color: #ce9178;
}
.markdown-container :global(.token.operator),
.markdown-container :global(.token.entity),
.markdown-container :global(.token.url),
.markdown-container :global(.language-css .token.string),
.markdown-container :global(.style .token.string) {
color: #d4d4d4;
}
.markdown-container :global(.token.atrule),
.markdown-container :global(.token.attr-value),
.markdown-container :global(.token.keyword) {
color: #569cd6;
}
.markdown-container :global(.token.function) {
color: #dcdcaa;
}
.markdown-container :global(.token.regex),
.markdown-container :global(.token.important),
.markdown-container :global(.token.variable) {
color: #d16969;
}
.markdown-container :global(.token.important),
.markdown-container :global(.token.bold) {
font-weight: bold;
}
.markdown-container :global(.token.italic) {
font-style: italic;
}
.markdown-container :global(.token.constant) {
color: #9cdcfe;
}
.markdown-container :global(.token.class-name) {
color: #4ec9b0;
}
.markdown-container :global(.token.parameter) {
color: #9cdcfe;
}
.markdown-container :global(.token.interpolation) {
color: #9cdcfe;
}
.markdown-container :global(.token.punctuation.interpolation-punctuation) {
color: #569cd6;
}
.markdown-container :global(.token.entity) {
cursor: help;
}
/* Line highlighting */
.markdown-container :global(pre[data-line]) {
position: relative;
}
.markdown-container :global(pre[class*="language-"] > code[class*="language-"]) {
position: relative;
z-index: 1;
}
</style>

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,661 @@
<script lang="ts">
import { TABotTokenStore, TAServerPort, TAServerUrl, authTokenStore } from "$lib/stores";
import { onMount, onDestroy } from "svelte";
import { bkAPIUrl } from '$lib/config.json';
//@ts-ignore
import { Match, Tournament, TAClient, Response_ResponseType } from 'moons-ta-client';
import { writable } from "svelte/store";
import SideMenu from "$lib/components/menus/SideMenuTournaments.svelte";
export let data;
const tournamentGuid: string = data.tournamentGuid;
let tournament: Tournament | undefined;
let isLoading = false;
let error: string | null = null;
let matches: Match[] = [];
let availablePlayers: any[] = [];
let selectedPlayers: any[] = [];
// Store for selected player guids
const selectedPlayerGuids = writable<string[]>([]);
// Player status enum
enum PlayerStatus {
Downloading = "downloading",
SelectingSong = "selecting",
Idle = "idle"
}
enum PlayerPlayState {
In_Menu = 0,
Waiting_For_Coordinator = 1,
In_Game = 2
}
const client = new TAClient();
// Remove 'Bearer ' from the token
if($authTokenStore) {
const cleanToken = $authTokenStore.replace('Bearer ', '');
client.setAuthToken(cleanToken);
}
// Handle player selection
function togglePlayerSelection(player: any) {
$selectedPlayerGuids = $selectedPlayerGuids.includes(player.guid)
? $selectedPlayerGuids.filter(guid => guid !== player.guid)
: [...$selectedPlayerGuids, player.guid];
}
// Return status color based on player status
function getStatusColor(status: PlayerStatus): string {
switch (status) {
case PlayerStatus.Downloading:
return "var(--danger-color)";
case PlayerStatus.SelectingSong:
return "#FCD34D"; // Yellow
case PlayerStatus.Idle:
return "#10B981"; // Green
default:
return "var(--text-secondary)";
}
}
// Handle match status changes
const handleMatchUpdated = (params: any) => {
console.log(params);
if (params.guid === tournamentGuid) {
// Update matches list
fetchTournamentData();
}
};
// Handle match deletion
const handleMatchDeleted = (matchInfo: [Match, Tournament]) => {
if (matchInfo[1].guid === tournamentGuid) {
// Update matches list
fetchTournamentData();
}
};
async function fetchTournamentData() {
if (!$authTokenStore) {
window.location.href = '/discordAuth';
return;
}
isLoading = true;
error = null;
try {
if(!client.isConnected) {
const connectResult = await client.connect($TAServerUrl, $TAServerPort);
if (connectResult.details.oneofKind === "connect" && connectResult.type === Response_ResponseType.Fail) {
throw new Error(connectResult.details.connect.message);
}
const joinResult = await client.joinTournament(tournamentGuid);
if (joinResult.details.oneofKind !== 'join' || joinResult.type === Response_ResponseType.Fail) {
throw new Error('Could not join tournament');
}
tournament = joinResult.details.join.state?.tournaments.find((x: Tournament) => x.guid === tournamentGuid);
console.log("tournament", tournament)
}
// Wait for state to be fully initialized
await new Promise(resolve => setTimeout(resolve, 1000));
// Get all matches for this tournament
matches = client.stateManager.getMatches(tournamentGuid) || [];
// Get all users for this tournament
const allUsers = client.stateManager.getUsers(tournamentGuid)!.filter(x => x.clientType === 0) || [];
// Get users not in matches
let allUsersGuidArray = allUsers.map(user => user.guid);
const usersInMatches = new Set();
matches.forEach(match => {
match.associatedUsers.forEach(async(userGuid) => {
if(allUsersGuidArray.includes(userGuid)){
let discordInfo = await client.getDiscordInfo(tournamentGuid, userGuid);
console.log("discordInfo", discordInfo)
console.log(await client.stateManager.getUser(tournamentGuid, (discordInfo as any).details.getDiscordInfo.discordId))
usersInMatches.add(userGuid);
}
});
});
availablePlayers = allUsers.filter(user => !usersInMatches.has(user.guid));
} catch (err) {
console.error('Error fetching tournament data:', err);
error = err instanceof Error ? err.message : 'An unknown error occurred';
matches = [];
availablePlayers = [];
} finally {
isLoading = false;
}
}
onMount(async() => {
if ($authTokenStore) {
await fetchTournamentData();
} else {
window.location.href = "/discordAuth"
}
client.on('createdMatch', handleMatchUpdated);
// client.on('deletedMatch', handleMatchDeleted);
});
onDestroy(() => {
// client.stateManager.removeListener('matchUpdated', handleMatchUpdated);
// client.stateManager.removeListener('matchDeleted', handleMatchDeleted);
client.removeListener('createdMatch', handleMatchUpdated)
client.disconnect();
});
</script>
<div class="layout">
<main class="dashboard">
<SideMenu currentPage="matches" tournamentName={tournament?.settings?.tournamentName} tournamnentGuid={tournamentGuid} />
<div class="content-area">
<div class="content-header">
<h2>Matches</h2>
<div class="actions">
<button class="action-button refresh" on:click={fetchTournamentData}>
<span class="material-icons">refresh</span>
</button>
</div>
</div>
{#if isLoading}
<div class="loading">
<div class="spinner"></div>
<p>Loading tournament data...</p>
</div>
{:else if error}
<div class="error-container">
<span class="material-icons">error_outline</span>
<p>{error}</p>
<button class="retry-button" on:click={fetchTournamentData}>Retry</button>
</div>
{:else}
<div class="dashboard-grid">
<div class="match-list">
<h3>Active Matches</h3>
{#if matches.length === 0}
<div class="empty-state">
<span class="material-icons">sports_esports</span>
<p>No active matches</p>
</div>
{:else}
<div class="match-cards">
{#each matches as match}
<div class="match-card">
<div class="match-header">
<div class="match-status" style="background-color: {match.selectedMap ? 'var(--danger-color)' : '#FCD34D'}"></div>
<h4>{'Unnamed Match'}</h4>
<!--
FIX THIS
-->
</div>
<div class="match-players">
{#if match.associatedUsers && match.associatedUsers.length > 0}
{#each match.associatedUsers as userGuid}
{#if client.stateManager.getUser(tournamentGuid, userGuid)}
<div class="player-chip">
<img src={client.stateManager.getUser(tournamentGuid, userGuid)?.discordInfo?.avatarUrl || "https://api.dicebear.com/7.x/identicon/svg?seed=" + userGuid}
alt="Player"
class="player-avatar">
<span>{client.stateManager.getUser(tournamentGuid, userGuid)?.name || 'Unknown Player'}</span>
</div>
{/if}
{/each}
{:else}
<p class="no-players">No players assigned</p>
{/if}
</div>
<div class="match-actions">
<a href={`/casting/${tournamentGuid}/${match.guid}`} class="match-action-button">
<span class="material-icons">visibility</span>
View
</a>
</div>
</div>
{/each}
</div>
{/if}
</div>
<div class="player-list">
<h3>Available Players</h3>
<div class="search-container">
<span class="material-icons">search</span>
<input type="text" placeholder="Search players..." class="search-input">
</div>
{#if availablePlayers.length === 0}
<div class="empty-state">
<span class="material-icons">person_off</span>
<p>No available players</p>
</div>
{:else}
<div class="player-cards">
<!-- svelte-ignore a11y-click-events-have-key-events -->
{#each availablePlayers as player}
{@const isSelected = $selectedPlayerGuids.includes(player.guid)}
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div class="player-card {isSelected ? 'selected' : ''}"
on:click={() => togglePlayerSelection(player)}>
<div class="player-status" style="background-color: {getStatusColor(player.playState)}"></div>
<img src={player.discordInfo?.avatarUrl || "https://api.dicebear.com/7.x/identicon/svg?seed=" + player.guid}
alt="Player"
class="player-avatar">
<div class="player-info">
<h4>{player.discordInfo.username}</h4>
<p>{PlayerPlayState[player.playState]}</p>
</div>
</div>
{/each}
</div>
{/if}
</div>
</div>
{#if $selectedPlayerGuids.length > 0}
<div class="selection-bar">
<p>{$selectedPlayerGuids.length} players selected</p>
<div class="selection-actions">
<button class="action-button create-match">
<span class="material-icons">add</span>
Create Match with Selection
</button>
<button class="action-button cancel" on:click={() => $selectedPlayerGuids = []}>
<span class="material-icons">close</span>
Clear Selection
</button>
</div>
</div>
{/if}
{/if}
</div>
</main>
</div>
<style>
/* Global styles are inherited from the attached CSS */
/* Dashboard layout */
.dashboard {
display: flex;
height: calc(100vh - var(--navbar-height));
}
.content-area {
flex: 1;
padding: 1.5rem;
overflow-y: auto;
}
.content-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1.5rem;
}
.content-header h2 {
font-size: 1.75rem;
font-weight: 600;
}
.actions {
display: flex;
gap: 0.75rem;
}
.action-button {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem 1rem;
border-radius: 0.375rem;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
border: none;
background-color: var(--bg-tertiary);
color: var(--text-primary);
}
.action-button.create {
background-color: var(--accent-color);
}
.action-button.create:hover {
background-color: var(--accent-hover);
box-shadow: 0 0 10px var(--accent-glow);
}
.action-button.refresh:hover {
background-color: var(--bg-secondary);
}
/* Dashboard grid */
.dashboard-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1.5rem;
}
/* Match list */
.match-list, .player-list {
background-color: var(--bg-secondary);
border-radius: 0.75rem;
padding: 1.5rem;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
.match-list h3, .player-list h3 {
font-size: 1.25rem;
font-weight: 500;
margin-bottom: 1rem;
}
.match-cards, .player-cards {
display: flex;
flex-direction: column;
gap: 1rem;
max-height: 60vh;
overflow-y: auto;
}
/* Match card */
.match-card {
background-color: var(--bg-tertiary);
border-radius: 0.5rem;
padding: 1rem;
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.match-header {
display: flex;
align-items: center;
gap: 0.75rem;
}
.match-status {
width: 0.75rem;
height: 0.75rem;
border-radius: 50%;
flex-shrink: 0;
}
.match-header h4 {
font-weight: 500;
font-size: 1rem;
}
.match-players {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
}
.player-chip {
display: flex;
align-items: center;
gap: 0.375rem;
padding: 0.25rem 0.5rem;
background-color: var(--bg-primary);
border-radius: 0.25rem;
font-size: 0.875rem;
}
.player-avatar {
width: 1.5rem;
height: 1.5rem;
border-radius: 50%;
object-fit: cover;
}
.no-players {
color: var(--text-secondary);
font-size: 0.875rem;
font-style: italic;
}
.match-actions {
display: flex;
justify-content: flex-end;
margin-top: 0.5rem;
}
.match-action-button {
display: flex;
align-items: center;
gap: 0.375rem;
padding: 0.375rem 0.75rem;
background-color: var(--accent-color);
color: var(--text-primary);
border-radius: 0.375rem;
text-decoration: none;
font-size: 0.875rem;
font-weight: 500;
transition: all 0.2s ease;
}
.match-action-button:hover {
background-color: var(--accent-hover);
box-shadow: 0 0 10px var(--accent-glow);
}
/* Player list */
.search-container {
display: flex;
align-items: center;
background-color: var(--bg-tertiary);
border-radius: 0.375rem;
padding: 0.5rem 0.75rem;
margin-bottom: 1rem;
}
.search-input {
background: none;
border: none;
outline: none;
color: var(--text-primary);
font-size: 0.875rem;
width: 100%;
margin-left: 0.5rem;
}
/* Player card */
.player-card {
display: flex;
align-items: center;
gap: 1rem;
padding: 0.75rem;
background-color: var(--bg-tertiary);
border-radius: 0.5rem;
cursor: pointer;
transition: all 0.2s ease;
border: 2px solid transparent;
}
.player-card:hover {
background-color: var(--bg-primary);
}
.player-card.selected {
border-color: var(--danger-color);
box-shadow: 0 0 10px rgba(239, 68, 68, 0.5);
}
.player-status {
width: 0.75rem;
height: 0.75rem;
border-radius: 50%;
flex-shrink: 0;
}
.player-card .player-avatar {
width: 2.5rem;
height: 2.5rem;
border-radius: 50%;
object-fit: cover;
}
.player-info {
display: flex;
flex-direction: column;
}
.player-info h4 {
font-size: 0.9375rem;
font-weight: 500;
}
.player-info p {
font-size: 0.75rem;
color: var(--text-secondary);
}
/* Empty state */
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 2rem;
color: var(--text-secondary);
gap: 0.75rem;
}
.empty-state .material-icons {
font-size: 2.5rem;
opacity: 0.6;
}
/* Loading */
.loading {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 50vh;
gap: 1rem;
}
.spinner {
width: 2.5rem;
height: 2.5rem;
border: 3px solid rgba(79, 70, 229, 0.2);
border-radius: 50%;
border-top-color: var(--accent-color);
animation: spin 1s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
/* Error container */
.error-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 2rem;
background-color: rgba(239, 68, 68, 0.1);
border-radius: 0.75rem;
color: var(--danger-color);
gap: 0.75rem;
}
.error-container .material-icons {
font-size: 2.5rem;
}
.retry-button {
padding: 0.5rem 1rem;
background-color: var(--danger-color);
color: white;
border: none;
border-radius: 0.375rem;
font-weight: 500;
cursor: pointer;
margin-top: 0.75rem;
}
.retry-button:hover {
background-color: var(--danger-hover);
}
/* Selection bar */
.selection-bar {
position: fixed;
bottom: 0;
left: 0;
right: 0;
background-color: var(--bg-secondary);
padding: 1rem 2rem;
display: flex;
justify-content: space-between;
align-items: center;
box-shadow: 0 -4px 12px rgba(0, 0, 0, 0.1);
z-index: 10;
}
.selection-actions {
display: flex;
gap: 0.75rem;
}
.action-button.create-match {
background-color: var(--accent-color);
}
.action-button.create-match:hover {
background-color: var(--accent-hover);
box-shadow: 0 0 10px var(--accent-glow);
}
.action-button.cancel {
background-color: var(--bg-tertiary);
}
.action-button.cancel:hover {
background-color: var(--danger-color);
}
/* Material Icons */
.material-icons {
font-family: 'Material Icons';
font-weight: normal;
font-style: normal;
font-size: 24px;
line-height: 1;
letter-spacing: normal;
text-transform: none;
display: inline-block;
white-space: nowrap;
word-wrap: normal;
direction: ltr;
-webkit-font-feature-settings: 'liga';
-webkit-font-smoothing: antialiased;
}
</style>

View file

@ -0,0 +1,6 @@
export function load({ params }: any){
return {
tournamentGuid: params.tournamentguid,
params: params
}
}

View file

@ -0,0 +1,707 @@
<script lang="ts">
import { TABotTokenStore, TAServerPort, TAServerUrl, authTokenStore, discordDataStore } from "$lib/stores";
import { onMount, onDestroy } from "svelte";
//@ts-ignore
import { Match, Tournament, TAClient, Response_ResponseType, Map } from 'moons-ta-client';
import SideMenu from "$lib/components/menus/SideMenuTournaments.svelte";
import InfoPopup from "$lib/components/notifications/InfoPopup.svelte";
import { bufferToImageUrl } from "$lib/services/taImages.js";
import { generateRandomHexColor } from "$lib/services/colours.js";
import { v4 as uuidv4 } from "uuid";
import Popup from "$lib/components/notifications/Popup.svelte";
import MapPoolModal from "$lib/components/popups/AddNewMapPool.svelte";
import { goto } from "$app/navigation";
export let data;
const tournamentGuid: string = data.tournamentGuid;
let tournament: Tournament | undefined;
let isLoading = false;
let error: string | null = null;
// MapPool data
interface MapPool {
guid: string;
name: string;
image: string;
color?: string;
}
interface TAMapPool {
guid: string;
name: string;
image: Uint8Array;
maps: Map[];
}
let mapPools: MapPool[] = [];
// Map pool modal state
let showMapPoolModal: boolean = false;
// Info popup state
let showInfoPopup: boolean = false;
let infoPopupContent: string = "";
let showSuccessfullySaved = false;
// Delete confirmation popup
let showDeleteConfirmation = false;
let mapPoolToDelete: string | null = null;
let mapPoolNameToDelete: string = "";
const client = new TAClient();
// Remove 'Bearer ' from the token
if ($authTokenStore) {
const cleanToken = $authTokenStore.replace('Bearer ', '');
client.setAuthToken(cleanToken);
}
function openInfoPopup(content: string) {
infoPopupContent = content;
showInfoPopup = true;
}
function closeInfoPopup() {
showInfoPopup = false;
}
function openMapPoolModal() {
showMapPoolModal = true;
}
function closeMapPoolModal() {
showMapPoolModal = false;
}
function closeNotification() {
showSuccessfullySaved = false;
}
function openDeleteConfirmation(poolGuid: string, poolName: string) {
mapPoolToDelete = poolGuid;
mapPoolNameToDelete = poolName;
showDeleteConfirmation = true;
}
function closeDeleteConfirmation() {
showDeleteConfirmation = false;
mapPoolToDelete = null;
mapPoolNameToDelete = "";
}
async function handleMapPoolCreated(event: CustomEvent<TAMapPool>) {
const newMapPool = event.detail;
let createMapPoolResponse = await client.addTournamentPool(tournamentGuid, newMapPool);
if(createMapPoolResponse.type !== Response_ResponseType.Success) {
error = "Failed to create the new map pool! Please refresh this page!";
} else {
let mapPoolImage;
try {
mapPoolImage = bufferToImageUrl(newMapPool.image);
} catch (error) {
mapPoolImage = "/talogo.png"
}
mapPools = [...mapPools, {
name: newMapPool.name,
image: mapPoolImage,
guid: newMapPool.guid,
color: generateRandomHexColor()
}];
showSuccessfullySaved = true;
}
}
async function editMapPool(poolGuid: string) {
goto(`/tournaments/${tournamentGuid}/mappools/${poolGuid}`);
}
async function deleteMapPool() {
if (!mapPoolToDelete) return;
let removeMapPoolResponse = await client.removeTournamentPool(tournamentGuid, mapPoolToDelete);
if(removeMapPoolResponse.type !== Response_ResponseType.Success) {
error = "Failed to remove map pool! Please refresh this page!";
} else {
mapPools = mapPools.filter(pool => pool.guid !== mapPoolToDelete);
}
closeDeleteConfirmation();
}
async function fetchMapPoolsData() {
if (!$authTokenStore) {
window.location.href = '/discordAuth';
return;
}
isLoading = true;
error = null;
try {
const connectResult = await client.connect($TAServerUrl, $TAServerPort);
if (connectResult.details.oneofKind === "connect" && connectResult.type === Response_ResponseType.Fail) {
throw new Error(connectResult.details.connect.message);
}
const joinResult = await client.joinTournament(tournamentGuid);
if (joinResult.details.oneofKind !== 'join' || joinResult.type === Response_ResponseType.Fail) {
throw new Error('Could not join tournament');
}
tournament = joinResult.details.join.state?.tournaments.find((x: Tournament) => x.guid === tournamentGuid);
if(tournament?.settings?.enablePools == false) {
error = "The map pools feature is not enabled for this tournament. Please enable it in the Settings section.";
return;
}
// Get map pools from tournament data
tournament?.settings?.pools.map(async (pool) => {
let mapPoolImage;
try {
mapPoolImage = bufferToImageUrl(pool.image);
} catch (error) {
mapPoolImage = "/talogo.png"
}
mapPools = [...mapPools, {
name: pool.name,
image: mapPoolImage,
guid: pool.guid,
color: generateRandomHexColor()
}];
});
} catch (err) {
console.error('Error fetching map pools data:', err);
error = err instanceof Error ? err.message : 'An unknown error occurred';
} finally {
isLoading = false;
}
}
onMount(async() => {
if ($authTokenStore) {
await fetchMapPoolsData();
} else {
window.location.href = "/discordAuth"
}
});
onDestroy(() => {
client.disconnect();
});
</script>
<div class="layout">
<main class="dashboard">
<SideMenu currentPage="mappools" tournamentName={tournament?.settings?.tournamentName} tournamnentGuid={tournamentGuid} />
<div class="content-area">
<div class="content-header">
<h2>Map Pools Management</h2>
<div class="actions">
{#if !error && !isLoading}
<button class="action-button add" on:click={openMapPoolModal}>
<span class="material-icons">add</span>
Create Map Pool
</button>
<button class="action-button refresh" on:click={fetchMapPoolsData}>
<span class="material-icons">refresh</span>
Refresh
</button>
{/if}
</div>
</div>
<div class="mappools-header">
<h3>Map Pools</h3>
<button class="info-button" on:click={() => openInfoPopup("Map pools allow you to organize maps into groups for different stages of your tournament. Each map pool can contain multiple maps with their own settings.")}>
<span class="material-icons">info</span>
</button>
</div>
{#if isLoading}
<div class="loading">
<div class="spinner"></div>
<p>Loading map pools data...</p>
</div>
{:else if error}
<div class="error-container">
<span class="material-icons">error_outline</span>
<p>{error}</p>
<button class="retry-button" on:click={fetchMapPoolsData}>Retry</button>
</div>
{:else if mapPools.length === 0}
<div class="no-mappools">
<span class="material-icons">map</span>
<p>No map pools have been created yet</p>
<button class="action-button add" on:click={openMapPoolModal}>
<span class="material-icons">add</span>
Create First Map Pool
</button>
</div>
{:else}
<div class="mappools-grid">
{#each mapPools as pool}
<div class="mappool-card" style="border-left: 4px solid {pool.color}">
<div class="mappool-header">
<div class="mappool-info">
<img
class="mappool-logo"
src={pool.image}
alt="{pool.name} logo"
/>
<div>
<h4>{pool.name}</h4>
</div>
</div>
<div class="mappool-actions">
<button
class="mappool-action edit"
title="Edit map pool"
on:click={() => editMapPool(pool.guid)}
>
<span class="material-icons">edit</span>
</button>
<button
class="mappool-action delete"
title="Delete map pool"
on:click={() => openDeleteConfirmation(pool.guid, pool.name)}
>
<span class="material-icons">delete</span>
</button>
</div>
</div>
</div>
{/each}
</div>
{/if}
</div>
</main>
</div>
<!-- Map Pool Modal Component -->
{#if showMapPoolModal}
<MapPoolModal
on:close={closeMapPoolModal}
on:mapPoolCreated={handleMapPoolCreated}
/>
{/if}
<!-- Info Popup -->
{#if showInfoPopup}
<InfoPopup content={infoPopupContent} onClose={closeInfoPopup} />
{/if}
<!-- Delete Confirmation Popup -->
{#if showDeleteConfirmation}
<div class="popup-overlay">
<div class="popup-container delete-confirmation">
<div class="popup-header">
<h3>Delete Map Pool</h3>
<button class="close-button" on:click={closeDeleteConfirmation}>
<span class="material-icons">close</span>
</button>
</div>
<div class="popup-content">
<p>Are you sure you want to delete the map pool <strong>{mapPoolNameToDelete}</strong>?</p>
<p class="warning">This action cannot be undone!</p>
<div class="popup-actions">
<button class="action-button cancel" on:click={closeDeleteConfirmation}>Cancel</button>
<button class="action-button delete" on:click={deleteMapPool}>
Delete Map Pool
</button>
</div>
</div>
</div>
</div>
{/if}
<Popup
bind:open={showSuccessfullySaved}
message="Successfully created a new map pool!"
icon="check"
iconColor="success"
buttons={[]}
on:close={closeNotification}
/>
<style>
/* Main layout */
.layout {
display: flex;
height: 100vh;
overflow: hidden;
}
.dashboard {
display: flex;
flex: 1;
overflow: hidden;
}
.content-area {
flex: 1;
padding: 2rem;
overflow-y: auto;
}
/* Content header */
.content-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1.5rem;
}
.content-header h2 {
font-size: 1.5rem;
font-weight: 600;
margin: 0;
}
.actions {
display: flex;
gap: 0.75rem;
}
.action-button {
display: flex;
align-items: center;
gap: 0.5rem;
background-color: var(--bg-secondary);
border: none;
border-radius: 0.375rem;
color: var(--text-primary);
font-weight: 500;
padding: 0.5rem 1rem;
cursor: pointer;
transition: background-color 0.2s;
}
.action-button.add,
.action-button.create {
background-color: var(--accent-color);
color: white;
}
.action-button.add:hover,
.action-button.create:hover {
background-color: var(--accent-hover);
}
.action-button.cancel {
background-color: var(--bg-secondary);
}
.action-button.cancel:hover {
background-color: var(--bg-tertiary);
}
.action-button.delete {
background-color: #ff5555;
color: white;
}
.action-button.delete:hover {
background-color: #ff3333;
}
.action-button.refresh:hover {
background-color: var(--bg-tertiary);
}
.action-button:disabled {
opacity: 0.6;
cursor: not-allowed;
}
/* Map Pools header with info button */
.mappools-header {
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 1rem;
}
.mappools-header h3 {
font-size: 1.25rem;
font-weight: 500;
margin: 0;
}
.info-button {
background: none;
border: none;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
color: var(--accent-color);
padding: 0;
}
.info-button .material-icons {
font-size: 1.125rem;
}
.info-button:hover {
color: var(--accent-hover);
}
/* Map Pools grid */
.mappools-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 1.5rem;
}
.mappool-card {
background-color: var(--bg-secondary);
border-radius: 0.5rem;
padding: 1.25rem;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.mappool-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
}
.mappool-info {
display: flex;
align-items: center;
gap: 1rem;
}
.mappool-logo {
width: 3rem;
height: 3rem;
border-radius: 0.375rem;
object-fit: cover;
background-color: var(--bg-tertiary);
}
.mappool-info h4 {
margin: 0;
font-size: 1.125rem;
font-weight: 500;
}
.mappool-actions {
display: flex;
gap: 0.5rem;
}
.mappool-action {
background: none;
border: none;
width: 2rem;
height: 2rem;
border-radius: 0.25rem;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
color: var(--text-secondary);
}
.mappool-action:hover {
background-color: var(--bg-tertiary);
color: var(--text-primary);
}
.mappool-action.edit:hover {
color: var(--accent-color);
}
.mappool-action.delete:hover {
color: #ff5555;
}
/* No map pools state */
.no-mappools {
background-color: var(--bg-secondary);
border-radius: 0.75rem;
padding: 3rem 2rem;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
}
.no-mappools .material-icons {
font-size: 3rem;
color: var(--text-secondary);
margin-bottom: 1rem;
}
.no-mappools p {
color: var(--text-secondary);
margin-bottom: 1.5rem;
}
/* Loading state */
.loading {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 3rem 0;
}
.spinner {
width: 3rem;
height: 3rem;
border: 0.25rem solid rgba(var(--accent-color-rgb), 0.2);
border-radius: 50%;
border-top-color: var(--accent-color);
animation: spin 1s linear infinite;
margin-bottom: 1rem;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
/* Error state */
.error-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 3rem 0;
text-align: center;
}
.error-container .material-icons {
font-size: 3rem;
color: #ff5555;
margin-bottom: 1rem;
}
.retry-button {
background-color: var(--accent-color);
border: none;
border-radius: 0.375rem;
color: white;
font-weight: 500;
padding: 0.5rem 1rem;
margin-top: 1rem;
cursor: pointer;
transition: background-color 0.2s;
}
.retry-button:hover {
background-color: var(--accent-hover);
}
/* Delete confirmation popup */
.popup-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
}
.popup-container {
background-color: var(--bg-primary);
border-radius: 0.75rem;
width: 90%;
max-width: 500px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.2);
}
.popup-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1.25rem 1.5rem;
border-bottom: 1px solid var(--bg-secondary);
}
.popup-header h3 {
margin: 0;
font-size: 1.25rem;
font-weight: 600;
}
.close-button {
background: none;
border: none;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
color: var(--text-secondary);
width: 2rem;
height: 2rem;
border-radius: 0.25rem;
}
.close-button:hover {
background-color: var(--bg-secondary);
color: var(--text-primary);
}
.popup-content {
padding: 1.5rem;
}
.popup-content p {
margin-top: 0;
}
.popup-content .warning {
color: #ff5555;
font-weight: 500;
}
.popup-actions {
display: flex;
justify-content: flex-end;
gap: 0.75rem;
margin-top: 1.5rem;
}
/* Responsive design */
@media (max-width: 768px) {
.mappools-grid {
grid-template-columns: 1fr;
}
.content-header {
flex-direction: column;
align-items: flex-start;
gap: 1rem;
}
.actions {
width: 100%;
}
.action-button {
flex: 1;
justify-content: center;
}
}
</style>

View file

@ -0,0 +1,6 @@
export function load({ params }: any){
return {
tournamentGuid: params.tournamentguid,
params: params
}
}

View file

@ -0,0 +1,923 @@
<script lang="ts">
import { onMount, onDestroy } from "svelte";
//@ts-ignore
import { TAClient, Response_ResponseType, Map, Tournament, GameplayModifiers_GameOptions } from 'moons-ta-client';
import { TABotTokenStore, TAServerPort, TAServerUrl, authTokenStore } from "$lib/stores";
import { bufferToImageUrl } from "$lib/services/taImages.js";
import SideMenu from "$lib/components/menus/SideMenuTournaments.svelte";
import Popup from "$lib/components/notifications/Popup.svelte";
import InfoPopup from "$lib/components/notifications/InfoPopup.svelte";
import EditMapModal from "$lib/components/popups/EditMap.svelte";
import { goto } from "$app/navigation";
export let data;
const tournamentGuid: string = data.tournamentGuid;
const poolGuid: string = data.poolGuid;
let tournament: Tournament | undefined;
let isLoading = false;
let error: string | null = null;
let poolName: string = "";
let maps: {
taData: Map,
beatsaverData: BeatSaverMap
}[] = [];
type BeatSaverMap = {
id: string;
metadata: {
songName: string;
songAuthorName: string;
levelAuthorName: string;
songSubName: string;
duration: number;
bpm: number;
};
versions: Array<{
hash: string;
state: string;
createdAt: string;
downloadURL: string;
diffs: Array<{
characteristic: string;
difficulty: string;
maxScore: number;
nps: number;
}>;
coverURL: string;
previewURL: string;
}>;
uploaded: string;
};
enum MapDifficulty {
"Easy" = 0,
"Normal" = 1,
"Hard" = 2,
"Expert" = 3,
"Expert+" = 4
}
interface Modifier {
id: GameplayModifiers_GameOptions;
name: string;
description: string;
enabled: boolean;
disabled?: boolean;
group?: string;
incompatibilites?: GameplayModifiers_GameOptions[];
}
let gameModifiers: Modifier[] = [
// Player modifiers
{ id: GameplayModifiers_GameOptions.NoFail, name: "No Fail", description: "You can't fail the level", enabled: false, group: "Player", incompatibilites: [GameplayModifiers_GameOptions.InstaFail, GameplayModifiers_GameOptions.BatteryEnergy] },
{ id: GameplayModifiers_GameOptions.InstaFail, name: "One Life", description: "You only have one life", enabled: false, group: "Player", incompatibilites: [GameplayModifiers_GameOptions.NoFail, GameplayModifiers_GameOptions.BatteryEnergy] },
{ id: GameplayModifiers_GameOptions.BatteryEnergy, name: "Four Lives", description: "You have four lives", enabled: false, group: "Player", incompatibilites: [GameplayModifiers_GameOptions.NoFail, GameplayModifiers_GameOptions.InstaFail] },
{ id: GameplayModifiers_GameOptions.NoBombs, name: "No Bombs", description: "No bombs will appear", enabled: false, group: "Player" },
{ id: GameplayModifiers_GameOptions.NoObstacles, name: "No Walls", description: "No walls will appear", enabled: false, group: "Player" },
{ id: GameplayModifiers_GameOptions.NoArrows, name: "No Arrows", description: "All notes can be cut in any direction", enabled: false, group: "Player", incompatibilites: [GameplayModifiers_GameOptions.DisappearingArrows] },
{ id: GameplayModifiers_GameOptions.GhostNotes, name: "Ghost Notes", description: "Note colors are hidden", enabled: false, group: "Player" },
{ id: GameplayModifiers_GameOptions.DisappearingArrows, name: "Disappearing Arrows", description: "Arrows disappear as they approach you", enabled: false, group: "Player", incompatibilites: [GameplayModifiers_GameOptions.NoArrows] },
// Speed modifiers
{ id: GameplayModifiers_GameOptions.SlowSong, name: "Slower Song", description: "Reduces the song speed by 15%", enabled: false, group: "Speed", incompatibilites: [GameplayModifiers_GameOptions.FastSong, GameplayModifiers_GameOptions.SuperFastSong] },
{ id: GameplayModifiers_GameOptions.FastSong, name: "Faster Song", description: "Increases the song speed by 20%", enabled: false, group: "Speed", incompatibilites: [GameplayModifiers_GameOptions.SlowSong, GameplayModifiers_GameOptions.SuperFastSong] },
{ id: GameplayModifiers_GameOptions.SuperFastSong, name: "Super Fast Song", description: "Increases the song speed by 50%", enabled: false, group: "Speed", incompatibilites: [GameplayModifiers_GameOptions.SlowSong, GameplayModifiers_GameOptions.FastSong] },
// Environment modifiers
{ id: GameplayModifiers_GameOptions.SmallCubes, name: "Small Notes", description: "Notes are smaller", enabled: false, group: "Environment", incompatibilites: [GameplayModifiers_GameOptions.ProMode] },
{ id: GameplayModifiers_GameOptions.ProMode, name: "Pro Mode", description: "Makes notes smaller, removes debris, and adds hit scores", enabled: false, group: "Environment", incompatibilites: [GameplayModifiers_GameOptions.SmallCubes] },
{ id: GameplayModifiers_GameOptions.ZenMode, name: "Zen Mode", description: "No fail, no bombs, reduced obstacles", enabled: false, group: "Environment" },
{ id: GameplayModifiers_GameOptions.StrictAngles, name: "Strict Angles", description: "Stricter angle enforcement for cuts", enabled: false, group: "Environment" }
];
// Edit Map Modal state
let showEditMapModal: boolean = false;
let currentEditMap: Map | null = null;
// Delete confirmation popup
let showDeleteConfirmation = false;
let mapToDelete: string | null = null;
// Info popup state
let showInfoPopup: boolean = false;
let infoPopupContent: string = "";
// Success notification
let showSuccessNotification = false;
let successMessage = "";
const client = new TAClient();
// Remove 'Bearer ' from the token
if ($authTokenStore) {
const cleanToken = $authTokenStore.replace('Bearer ', '');
client.setAuthToken(cleanToken);
}
function openInfoPopup(content: string) {
infoPopupContent = content;
showInfoPopup = true;
}
function closeInfoPopup() {
showInfoPopup = false;
}
function openEditMapModal(map: Map | null = null) {
currentEditMap = map;
showEditMapModal = true;
}
function closeEditMapModal() {
showEditMapModal = false;
currentEditMap = null;
}
function openDeleteConfirmation(mapId: string) {
mapToDelete = mapId;
showDeleteConfirmation = true;
}
function closeDeleteConfirmation() {
showDeleteConfirmation = false;
mapToDelete = null;
}
function closeNotification() {
showSuccessNotification = false;
}
function handleMapUpdated(event: CustomEvent) {
fetchMapPoolData();
showSuccessNotification = true;
successMessage = "Map successfully updated!";
}
function handleMapAdded(event: CustomEvent) {
fetchMapPoolData();
showSuccessNotification = true;
successMessage = "Map successfully added!";
}
async function deleteMap() {
if (!mapToDelete) return;
isLoading = true;
try {
// Implement actual map deletion - this will depend on your API
const removeMapResponse = await client.removeTournamentPoolMap(tournamentGuid, poolGuid, mapToDelete);
if (removeMapResponse.type !== Response_ResponseType.Success) {
throw new Error("Failed to remove map! Please refresh this page!");
}
maps = maps.filter(map => map.taData.guid !== mapToDelete);
showSuccessNotification = true;
successMessage = "Map successfully deleted!";
} catch (err) {
console.error('Error deleting map:', err);
error = err instanceof Error ? err.message : 'An unknown error occurred';
} finally {
isLoading = false;
closeDeleteConfirmation();
}
}
function goBackToMapPools() {
goto(`/tournaments/${tournamentGuid}/mappools`);
}
async function fetchMapPoolData() {
if (!$authTokenStore) {
window.location.href = '/discordAuth';
return;
}
isLoading = true;
error = null;
try {
if(!client.isConnected) {
const connectResult = await client.connect($TAServerUrl, $TAServerPort);
if (connectResult.details.oneofKind === "connect" && connectResult.type === Response_ResponseType.Fail) {
throw new Error(connectResult.details.connect.message);
}
const joinResult = await client.joinTournament(tournamentGuid);
if (joinResult.details.oneofKind !== 'join' || joinResult.type === Response_ResponseType.Fail) {
throw new Error('Could not join tournament');
}
tournament = await joinResult.details.join.state?.tournaments.find((x: Tournament) => x.guid === tournamentGuid);
} else {
tournament = await client.stateManager.getTournament(tournamentGuid)!;
}
// Find the map pool with the specified GUID
const pool = await tournament?.settings?.pools.find(p => p.guid === poolGuid);
if (!pool) {
throw new Error('Map pool not found');
}
poolName = pool.name;
await pool.maps.map(async(map) => {
console.log(map);
let beatsaverData = await getMapBeatsaverData(map.gameplayParameters!.beatmap!.levelId);
maps = [...maps, {
taData: map,
beatsaverData: beatsaverData
}];
});
maps = maps;
} catch (err) {
console.error('Error fetching map pool data:', err);
error = err instanceof Error ? err.message : 'An unknown error occurred';
} finally {
isLoading = false;
}
}
async function getMapBeatsaverData(levelId: string): Promise<BeatSaverMap> {
const newLevelId = levelId.replace("custom_level_", "");
const data = await fetch(`https://api.beatsaver.com/maps/hash/${newLevelId}`);
if(!data.ok) {
error = "Unable to fetch a map-s BeatSaver information. Please go to inspect, console, and take a screenshot and send it to serverbp or matrikmoon on Discord."
}
const response: BeatSaverMap = await data.json();
return response;
}
onMount(async() => {
if ($authTokenStore) {
await fetchMapPoolData();
} else {
window.location.href = "/discordAuth";
}
});
onDestroy(() => {
client.disconnect();
});
</script>
<div class="layout">
<main class="dashboard">
<SideMenu currentPage="mappools" tournamentName={tournament?.settings?.tournamentName} tournamnentGuid={tournamentGuid} />
<div class="content-area">
<div class="content-header">
<div class="header-left">
<button class="back-button" on:click={goBackToMapPools}>
<span class="material-icons">arrow_back</span>
Back to Map Pools
</button>
<h2>{poolName} Maps</h2>
</div>
<div class="actions">
{#if !error && !isLoading}
<button class="action-button add" on:click={() => openEditMapModal()}>
<span class="material-icons">add</span>
Add Map
</button>
<button class="action-button refresh" on:click={fetchMapPoolData}>
<span class="material-icons">refresh</span>
Refresh
</button>
{/if}
</div>
</div>
<div class="maps-header">
<h3>Maps in this Pool</h3>
<button class="info-button" on:click={() => openInfoPopup("Maps are Beat Saber songs that can be played in your tournament. Each map can have different modifiers and settings configured.")}>
<span class="material-icons">info</span>
</button>
</div>
{#if isLoading}
<div class="loading">
<div class="spinner"></div>
<p>Loading maps data...</p>
</div>
{:else if error}
<div class="error-container">
<span class="material-icons">error_outline</span>
<p>{error}</p>
<button class="retry-button" on:click={fetchMapPoolData}>Retry</button>
</div>
{:else if maps.length === 0}
<div class="no-maps">
<span class="material-icons">music_note</span>
<p>No maps have been added to this pool yet</p>
<button class="action-button add" on:click={() => openEditMapModal()}>
<span class="material-icons">add</span>
Add First Map
</button>
</div>
{:else}
<div class="maps-list">
{#each maps as map}
<div class="map-card">
<div class="map-content">
<div class="map-image">
<img
src={map.beatsaverData.versions[0].coverURL}
alt="{map.taData.gameplayParameters?.beatmap?.name} cover"
/>
</div>
<div class="map-info">
<h4>{map.taData.gameplayParameters?.beatmap?.name}</h4>
<p class="map-author">By {map.beatsaverData.metadata.levelAuthorName || "Unknown Artist"}</p>
<div class="map-details">
{#if map.taData.gameplayParameters?.beatmap?.difficulty}
<span class="map-difficulty">
<span class="material-icons">speed</span>
{MapDifficulty[map.taData.gameplayParameters?.beatmap?.difficulty]}
</span>
{/if}
<!-- {#if map.length}
<span class="map-length">
<span class="material-icons">access_time</span>
{map.length}
</span>
{/if} -->
</div>
</div>
<div class="map-actions">
<button
class="map-action edit"
title="Edit map"
on:click={() => openEditMapModal(map.taData)}
>
<span class="material-icons">edit</span>
</button>
<button
class="map-action delete"
title="Delete map"
on:click={() => openDeleteConfirmation(map.taData.guid)}
>
<span class="material-icons">delete</span>
</button>
</div>
</div>
<div class="map-modifiers">
<h5>Active Modifiers</h5>
<div class="modifier-tags">
<!-- {#if map.modifiers && map.modifiers.length > 0}
{#each map.modifiers as modifier}
<span class="modifier-tag">{modifier}</span>
{/each}
{:else}
<span class="no-modifiers">No modifiers active</span>
{/if} -->
</div>
</div>
</div>
{/each}
</div>
{/if}
</div>
</main>
</div>
<!-- Edit Map Modal Component -->
{#if showEditMapModal}
<EditMapModal
tournamentGuid={tournamentGuid}
poolGuid={poolGuid}
map={maps.find(map => map.taData.guid == currentEditMap?.guid)}
on:close={closeEditMapModal}
on:mapUpdated={handleMapUpdated}
on:mapAdded={handleMapAdded}
/>
{/if}
<!-- Info Popup -->
{#if showInfoPopup}
<InfoPopup content={infoPopupContent} onClose={closeInfoPopup} />
{/if}
<!-- Delete Confirmation Popup -->
{#if showDeleteConfirmation}
<div class="popup-overlay">
<div class="popup-container delete-confirmation">
<div class="popup-header">
<h3>Delete Map</h3>
<button class="close-button" on:click={closeDeleteConfirmation}>
<span class="material-icons">close</span>
</button>
</div>
<div class="popup-content">
<p>Are you sure you want to delete this map?</p>
<p class="warning">This action cannot be undone!</p>
<div class="popup-actions">
<button class="action-button cancel" on:click={closeDeleteConfirmation}>Cancel</button>
<button class="action-button delete" on:click={deleteMap}>
Delete Map
</button>
</div>
</div>
</div>
</div>
{/if}
<!-- Success Notification -->
<Popup
bind:open={showSuccessNotification}
message={successMessage}
icon="check"
iconColor="success"
buttons={[]}
on:close={closeNotification}
/>
<style>
/* Main layout */
.layout {
display: flex;
height: 100vh;
overflow: hidden;
}
.dashboard {
display: flex;
flex: 1;
overflow: hidden;
}
.content-area {
flex: 1;
padding: 2rem;
overflow-y: auto;
}
/* Content header */
.content-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1.5rem;
}
.header-left {
display: flex;
align-items: center;
gap: 1rem;
}
.back-button {
display: flex;
align-items: center;
gap: 0.5rem;
background: none;
border: none;
color: var(--accent-color);
font-weight: 500;
padding: 0.5rem 0;
cursor: pointer;
transition: color 0.2s;
margin-right: 2rem;
}
.back-button:hover {
color: var(--accent-hover);
}
.content-header h2 {
font-size: 1.5rem;
font-weight: 600;
margin: 0;
}
.actions {
display: flex;
gap: 0.75rem;
}
.action-button {
display: flex;
align-items: center;
gap: 0.5rem;
background-color: var(--bg-secondary);
border: none;
border-radius: 0.375rem;
color: var(--text-primary);
font-weight: 500;
padding: 0.5rem 1rem;
cursor: pointer;
transition: background-color 0.2s;
}
.action-button.add,
.action-button.create {
background-color: var(--accent-color);
color: white;
}
.action-button.add:hover,
.action-button.create:hover {
background-color: var(--accent-hover);
}
.action-button.cancel {
background-color: var(--bg-secondary);
}
.action-button.cancel:hover {
background-color: var(--bg-tertiary);
}
.action-button.delete {
background-color: #ff5555;
color: white;
}
.action-button.delete:hover {
background-color: #ff3333;
}
.action-button.refresh:hover {
background-color: var(--bg-tertiary);
}
.action-button:disabled {
opacity: 0.6;
cursor: not-allowed;
}
/* Maps header with info button */
.maps-header {
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 1rem;
}
.maps-header h3 {
font-size: 1.25rem;
font-weight: 500;
margin: 0;
}
.info-button {
background: none;
border: none;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
color: var(--accent-color);
padding: 0;
}
.info-button .material-icons {
font-size: 1.125rem;
}
.info-button:hover {
color: var(--accent-hover);
}
/* Maps list */
.maps-list {
display: flex;
flex-direction: column;
gap: 1rem;
}
.map-card {
background-color: var(--bg-secondary);
border-radius: 0.5rem;
padding: 1.25rem;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.map-content {
display: flex;
gap: 1.25rem;
}
.map-image {
flex-shrink: 0;
width: 6rem;
height: 6rem;
}
.map-image img {
width: 100%;
height: 100%;
object-fit: cover;
border-radius: 0.375rem;
background-color: var(--bg-tertiary);
}
.map-info {
flex: 1;
display: flex;
flex-direction: column;
}
.map-info h4 {
margin: 0 0 0.25rem 0;
font-size: 1.125rem;
font-weight: 500;
}
.map-author {
color: var(--text-secondary);
margin: 0 0 0.5rem 0;
font-size: 0.875rem;
}
.map-details {
display: flex;
gap: 1rem;
margin-top: 0.5rem;
}
.map-difficulty,
.map-length {
display: flex;
align-items: center;
gap: 0.25rem;
color: var(--text-secondary);
font-size: 0.875rem;
}
.map-difficulty .material-icons,
.map-length .material-icons {
font-size: 1rem;
}
.map-actions {
display: flex;
gap: 0.5rem;
margin-left: auto;
}
.map-action {
background: none;
border: none;
width: 2.5rem;
height: 2.5rem;
border-radius: 0.25rem;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
color: var(--text-secondary);
}
.map-action:hover {
background-color: var(--bg-tertiary);
color: var(--text-primary);
}
.map-action.edit:hover {
color: var(--accent-color);
}
.map-action.delete:hover {
color: #ff5555;
}
.map-modifiers {
margin-top: 1rem;
padding-top: 1rem;
border-top: 1px solid var(--bg-tertiary);
}
.map-modifiers h5 {
margin: 0 0 0.5rem 0;
font-size: 0.875rem;
font-weight: 500;
color: var(--text-secondary);
}
.modifier-tags {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
}
.modifier-tag {
background-color: var(--bg-tertiary);
padding: 0.25rem 0.5rem;
border-radius: 1rem;
font-size: 0.75rem;
}
.no-modifiers {
color: var(--text-secondary);
font-size: 0.875rem;
font-style: italic;
}
/* No maps state */
.no-maps {
background-color: var(--bg-secondary);
border-radius: 0.75rem;
padding: 3rem 2rem;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
}
.no-maps .material-icons {
font-size: 3rem;
color: var(--text-secondary);
margin-bottom: 1rem;
}
.no-maps p {
color: var(--text-secondary);
margin-bottom: 1.5rem;
}
/* Loading state */
.loading {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 3rem 0;
}
.spinner {
width: 3rem;
height: 3rem;
border: 0.25rem solid rgba(var(--accent-color-rgb), 0.2);
border-radius: 50%;
border-top-color: var(--accent-color);
animation: spin 1s linear infinite;
margin-bottom: 1rem;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
/* Error state */
.error-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 3rem 0;
text-align: center;
}
.error-container .material-icons {
font-size: 3rem;
color: #ff5555;
margin-bottom: 1rem;
}
.retry-button {
background-color: var(--accent-color);
border: none;
border-radius: 0.375rem;
color: white;
font-weight: 500;
padding: 0.5rem 1rem;
margin-top: 1rem;
cursor: pointer;
transition: background-color 0.2s;
}
.retry-button:hover {
background-color: var(--accent-hover);
}
/* Delete confirmation popup */
.popup-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
}
.popup-container {
background-color: var(--bg-primary);
border-radius: 0.75rem;
width: 90%;
max-width: 500px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.2);
}
.popup-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1.25rem 1.5rem;
border-bottom: 1px solid var(--bg-secondary);
}
.popup-header h3 {
margin: 0;
font-size: 1.25rem;
font-weight: 600;
}
.close-button {
background: none;
border: none;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
color: var(--text-secondary);
width: 2rem;
height: 2rem;
border-radius: 0.25rem;
}
.close-button:hover {
background-color: var(--bg-secondary);
color: var(--text-primary);
}
.popup-content {
padding: 1.5rem;
}
.popup-content p {
margin-top: 0;
}
.popup-content .warning {
color: #ff5555;
font-weight: 500;
}
.popup-actions {
display: flex;
justify-content: flex-end;
gap: 0.75rem;
margin-top: 1.5rem;
}
/* Responsive design */
@media (max-width: 768px) {
.content-header {
flex-direction: column;
align-items: flex-start;
gap: 1rem;
}
.header-left {
flex-direction: column;
align-items: flex-start;
gap: 0.5rem;
}
.actions {
width: 100%;
}
.action-button {
flex: 1;
justify-content: center;
}
.map-content {
flex-direction: column;
}
.map-image {
width: 100%;
height: auto;
aspect-ratio: 1;
max-width: 10rem;
margin: 0 auto 1rem;
}
.map-actions {
margin-left: 0;
margin-top: 1rem;
justify-content: flex-end;
}
}
</style>

View file

@ -0,0 +1,7 @@
export function load({ params }: any){
return {
tournamentGuid: params.tournamentguid,
poolGuid: params.poolGuid,
params: params
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,6 @@
export function load({ params }: any){
return {
tournamentGuid: params.tournamentguid,
params: params
}
}

View file

@ -0,0 +1,852 @@
<script lang="ts">
import { TABotTokenStore, TAServerPort, TAServerUrl, authTokenStore, discordDataStore } from "$lib/stores";
import { onMount, onDestroy } from "svelte";
//@ts-ignore
import { Match, Tournament, TAClient, Response_ResponseType } from 'moons-ta-client';
import SideMenu from "$lib/components/menus/SideMenuTournaments.svelte";
import InfoPopup from "$lib/components/notifications/InfoPopup.svelte";
import { bufferToImageUrl, convertImageToUint8Array, linkToUint8Array } from "$lib/services/taImages.js";
import { generateRandomHexColor } from "$lib/services/colours.js";
import { v4 as uuidv4 } from "uuid";
import Popup from "$lib/components/notifications/Popup.svelte";
export let data;
const tournamentGuid: string = data.tournamentGuid;
let tournament: Tournament | undefined;
let isLoading = false;
let error: string | null = null;
// Teams data
interface Team {
guid: string;
name: string;
image: string;
color?: string;
}
interface TATeam {
guid: string;
name: string;
image: Uint8Array;
}
let teams: Team[] = [];
// New team popup state
let showTeamPopup: boolean = false;
let newTeamName: string = "";
let newTeamDescription: string = "";
let newTeamColor: string = "#3366ff"; // Default color
let teamImageBuffer: Uint8Array | null = null;
let teamImagePreview: string | null = null;
// Info popup state
let showInfoPopup: boolean = false;
let infoPopupContent: string = "";
let showSuccessfullySaved = false;
// File upload
let fileInput: HTMLInputElement;
const client = new TAClient();
// Remove 'Bearer ' from the token
if ($authTokenStore) {
const cleanToken = $authTokenStore.replace('Bearer ', '');
client.setAuthToken(cleanToken);
}
function openInfoPopup(content: string) {
infoPopupContent = content;
showInfoPopup = true;
}
function closeInfoPopup() {
showInfoPopup = false;
}
function openTeamPopup() {
// Reset form fields
newTeamName = "";
newTeamDescription = "";
newTeamColor = "#3366ff";
teamImageBuffer = null;
teamImagePreview = null;
showTeamPopup = true;
}
function closeTeamPopup() {
showTeamPopup = false;
}
function handleImageUpload() {
fileInput.click();
}
function closeNotification() {
showSuccessfullySaved = false;
}
function onFileSelected(event: Event) {
const target = event.target as HTMLInputElement;
const files = target.files;
if (files && files.length > 0) {
const file = files[0];
const reader = new FileReader();
reader.onload = (e) => {
const result = e.target?.result;
if (result instanceof ArrayBuffer) {
teamImageBuffer = new Uint8Array(result);
// Create preview URL
const blob = new Blob([teamImageBuffer], { type: file.type });
teamImagePreview = URL.createObjectURL(blob);
}
};
reader.readAsArrayBuffer(file);
}
}
async function createTeam() {
// Add team to the list
const newTeam: TATeam = {
guid: uuidv4(),
name: newTeamName,
image: teamImageBuffer || new Uint8Array(1)
};
console.log(newTeam);
let createTeamResponse = await client.addTournamentTeam(tournamentGuid, newTeam);
if(createTeamResponse.type !== Response_ResponseType.Success) {
error = "Failed to create the new team! Please refresh this page!";
} else {
let teamImage;
try {
teamImage = bufferToImageUrl(teamImageBuffer || new Uint8Array(1));
} catch (error) {
teamImage = "/talogo.png"
}
await teams.push({
name: newTeam.name,
image: teamImage,
guid: newTeam.guid,
color: generateRandomHexColor()
});
}
// Close the popup
closeTeamPopup();
}
async function deleteTeam(teamGuid: string) {
let removeTeamResponse = await client.removeTournamentTeam(tournamentGuid, teamGuid);
if(removeTeamResponse.type !== Response_ResponseType.Success) {
error = "Failed to remove team! Please refresh this page!";
} else {
// Remove team from the list
teams = teams.filter(team => team.guid !== teamGuid);
}
}
async function fetchTeamsData() {
if (!$authTokenStore) {
window.location.href = '/discordAuth';
return;
}
isLoading = true;
error = null;
try {
const connectResult = await client.connect($TAServerUrl, $TAServerPort);
if (connectResult.details.oneofKind === "connect" && connectResult.type === Response_ResponseType.Fail) {
throw new Error(connectResult.details.connect.message);
}
const joinResult = await client.joinTournament(tournamentGuid);
if (joinResult.details.oneofKind !== 'join' || joinResult.type === Response_ResponseType.Fail) {
throw new Error('Could not join tournament');
}
tournament = joinResult.details.join.state?.tournaments.find((x: Tournament) => x.guid === tournamentGuid);
if(tournament?.settings?.enableTeams == false) {
error = "The teams feature is not enabled for this tournament. Please enable it in the Settings section.";
return;
}
console.log(tournament)
// For demo purposes, we'll create some sample teams
// In a real app, this would come from the tournament data
tournament?.settings?.teams.map(async (team) => {
let teamImage;
try {
teamImage = bufferToImageUrl(team.image);
} catch (error) {
teamImage = "/talogo.png"
}
await teams.push({
name: team.name,
image: teamImage,
guid: team.guid,
color: generateRandomHexColor()
});
});
} catch (err) {
console.error('Error fetching teams data:', err);
error = err instanceof Error ? err.message : 'An unknown error occurred';
} finally {
isLoading = false;
}
}
onMount(async() => {
if ($authTokenStore) {
await fetchTeamsData();
} else {
window.location.href = "/discordAuth"
}
});
onDestroy(() => {
client.disconnect();
});
</script>
<div class="layout">
<main class="dashboard">
<SideMenu currentPage="teams" tournamentName={tournament?.settings?.tournamentName} tournamnentGuid={tournamentGuid} />
<div class="content-area">
<div class="content-header">
<h2>Teams Management</h2>
<div class="actions">
{#if !error && !isLoading}
<button class="action-button add" on:click={openTeamPopup}>
<span class="material-icons">add</span>
Create Team
</button>
<button class="action-button refresh" on:click={fetchTeamsData}>
<span class="material-icons">refresh</span>
Refresh
</button>
{/if}
</div>
</div>
<div class="teams-header">
<h3>Teams</h3>
<button class="info-button" on:click={() => openInfoPopup("Teams allow players to be organized into groups during the tournament. Each team can have its own name, description, color, and logo.")}>
<span class="material-icons">info</span>
</button>
</div>
{#if isLoading}
<div class="loading">
<div class="spinner"></div>
<p>Loading teams data...</p>
</div>
{:else if error}
<div class="error-container">
<span class="material-icons">error_outline</span>
<p>{error}</p>
<button class="retry-button" on:click={fetchTeamsData}>Retry</button>
</div>
{:else if teams.length === 0}
<div class="no-teams">
<span class="material-icons">groups</span>
<p>No teams have been created yet</p>
<button class="action-button add" on:click={openTeamPopup}>
<span class="material-icons">add</span>
Create First Team
</button>
</div>
{:else}
<div class="teams-grid">
{#each teams as team}
<div class="team-card" style="border-left: 4px solid {team.color}">
<div class="team-header">
<div class="team-info">
<img
class="team-logo"
src={team.image}
alt="{team.name} logo"
/>
<div>
<h4>{team.name}</h4>
</div>
</div>
<div class="team-actions">
<button
class="team-action delete"
title="Delete team"
on:click={() => deleteTeam(team.guid)}
>
<span class="material-icons">delete</span>
</button>
</div>
</div>
</div>
{/each}
</div>
{/if}
</div>
</main>
</div>
<!-- Create Team Popup -->
{#if showTeamPopup}
<div class="popup-overlay">
<div class="popup-container">
<div class="popup-header">
<h3>Create New Team</h3>
<button class="close-button" on:click={closeTeamPopup}>
<span class="material-icons">close</span>
</button>
</div>
<div class="popup-content">
<div class="form-group">
<label for="team-name">Team Name</label>
<input
type="text"
id="team-name"
class="form-input"
bind:value={newTeamName}
placeholder="Enter team name"
required
/>
</div>
<div class="form-group">
<!-- svelte-ignore a11y-label-has-associated-control -->
<label>Team Logo (Optional)</label>
<div class="image-upload-container">
<div class="image-preview">
{#if teamImagePreview}
<img src={teamImagePreview} alt="Team logo preview" />
<!-- svelte-ignore a11y-no-noninteractive-element-interactions -->
<!-- svelte-ignore a11y-no-static-element-interactions -->
{:else}
<!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-noninteractive-element-interactions -->
<img on:click={handleImageUpload} src="/talogo.png" alt="Default logo" />
<!-- svelte-ignore a11y-no-static-element-interactions -->
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div on:click={handleImageUpload} class="upload-overlay">
<span class="material-icons">add_photo_alternate</span>
</div>
{/if}
</div>
<button class="action-button upload" on:click={handleImageUpload}>
<span class="material-icons">upload</span>
{teamImagePreview ? 'Change Logo' : 'Upload Logo'}
</button>
<input
type="file"
accept="image/*"
style="display: none"
bind:this={fileInput}
on:change={onFileSelected}
/>
</div>
</div>
<div class="popup-actions">
<button class="action-button cancel" on:click={closeTeamPopup}>Cancel</button>
<button
class="action-button create"
on:click={createTeam}
disabled={!newTeamName}
>
Create Team
</button>
</div>
</div>
</div>
</div>
{/if}
<!-- Info Popup -->
{#if showInfoPopup}
<InfoPopup content={infoPopupContent} onClose={closeInfoPopup} />
{/if}
<Popup
bind:open={showSuccessfullySaved}
message="Successfully saved the tournament information!"
icon="check"
iconColor="success"
buttons={[]}
on:close={closeNotification}
/>
<style>
/* Main layout */
.layout {
display: flex;
height: 100vh;
overflow: hidden;
}
.dashboard {
display: flex;
flex: 1;
overflow: hidden;
}
.content-area {
flex: 1;
padding: 2rem;
overflow-y: auto;
}
/* Content header */
.content-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1.5rem;
}
.content-header h2 {
font-size: 1.5rem;
font-weight: 600;
margin: 0;
}
.actions {
display: flex;
gap: 0.75rem;
}
.action-button {
display: flex;
align-items: center;
gap: 0.5rem;
background-color: var(--bg-secondary);
border: none;
border-radius: 0.375rem;
color: var(--text-primary);
font-weight: 500;
padding: 0.5rem 1rem;
cursor: pointer;
transition: background-color 0.2s;
}
.action-button.add,
.action-button.create {
background-color: var(--accent-color);
color: white;
}
.action-button.add:hover,
.action-button.create:hover {
background-color: var(--accent-hover);
}
.action-button.cancel {
background-color: var(--bg-secondary);
}
.action-button.cancel:hover {
background-color: var(--bg-tertiary);
}
.action-button.refresh:hover {
background-color: var(--bg-tertiary);
}
.action-button.upload {
background-color: var(--accent-color);
color: white;
}
.action-button:disabled {
opacity: 0.6;
cursor: not-allowed;
}
/* Teams header with info button */
.teams-header {
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 1rem;
}
.teams-header h3 {
font-size: 1.25rem;
font-weight: 500;
margin: 0;
}
.info-button {
background: none;
border: none;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
color: var(--accent-color);
padding: 0;
}
.info-button .material-icons {
font-size: 1.125rem;
}
.info-button:hover {
color: var(--accent-hover);
}
/* Teams grid */
.teams-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 1.5rem;
}
.team-card {
background-color: var(--bg-secondary);
border-radius: 0.5rem;
padding: 1.25rem;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.team-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
}
.team-info {
display: flex;
align-items: center;
gap: 1rem;
}
.team-logo {
width: 3rem;
height: 3rem;
border-radius: 0.375rem;
object-fit: cover;
background-color: var(--bg-tertiary);
}
.team-info h4 {
margin: 0;
font-size: 1.125rem;
font-weight: 500;
}
.team-description {
margin: 0.25rem 0 0;
font-size: 0.875rem;
color: var(--text-secondary);
max-width: 200px;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
}
.team-actions {
display: flex;
gap: 0.5rem;
}
.team-action {
background: none;
border: none;
width: 2rem;
height: 2rem;
border-radius: 0.25rem;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
color: var(--text-secondary);
}
.team-action:hover {
background-color: var(--bg-tertiary);
color: var(--text-primary);
}
.team-action.delete:hover {
color: #ff5555;
}
/* No teams state */
.no-teams {
background-color: var(--bg-secondary);
border-radius: 0.75rem;
padding: 3rem 2rem;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
}
.no-teams .material-icons {
font-size: 3rem;
color: var(--text-secondary);
margin-bottom: 1rem;
}
.no-teams p {
color: var(--text-secondary);
margin-bottom: 1.5rem;
}
/* Loading state */
.loading {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 3rem 0;
}
.spinner {
width: 3rem;
height: 3rem;
border: 0.25rem solid rgba(var(--accent-color-rgb), 0.2);
border-radius: 50%;
border-top-color: var(--accent-color);
animation: spin 1s linear infinite;
margin-bottom: 1rem;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
/* Error state */
.error-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 3rem 0;
text-align: center;
}
.error-container .material-icons {
font-size: 3rem;
color: #ff5555;
margin-bottom: 1rem;
}
.retry-button {
background-color: var(--accent-color);
border: none;
border-radius: 0.375rem;
color: white;
font-weight: 500;
padding: 0.5rem 1rem;
margin-top: 1rem;
cursor: pointer;
transition: background-color 0.2s;
}
.retry-button:hover {
background-color: var(--accent-hover);
}
/* Create Team Popup */
.popup-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
}
.popup-container {
background-color: var(--bg-primary);
border-radius: 0.75rem;
width: 90%;
max-width: 500px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.2);
}
.popup-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1.25rem 1.5rem;
border-bottom: 1px solid var(--bg-secondary);
}
.popup-header h3 {
margin: 0;
font-size: 1.25rem;
font-weight: 600;
}
.close-button {
background: none;
border: none;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
color: var(--text-secondary);
width: 2rem;
height: 2rem;
border-radius: 0.25rem;
}
.close-button:hover {
background-color: var(--bg-secondary);
color: var(--text-primary);
}
.popup-content {
padding: 1.5rem;
}
.form-group {
margin-bottom: 1.25rem;
}
.form-group label {
display: block;
margin-bottom: 0.5rem;
font-weight: 500;
font-size: 0.9375rem;
}
.form-input {
width: 100%;
padding: 0.625rem 0.75rem;
background-color: var(--bg-tertiary);
border: none;
border-radius: 0.375rem;
color: var(--text-primary);
font-size: 0.9375rem;
font-family: inherit;
}
.form-input:focus {
outline: 2px solid var(--accent-color);
}
/* Color picker */
.color-picker-container {
display: flex;
align-items: center;
gap: 1rem;
}
.color-picker {
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
width: 3rem;
height: 3rem;
border: none;
border-radius: 0.375rem;
background-color: transparent;
cursor: pointer;
}
.color-picker::-webkit-color-swatch-wrapper {
padding: 0;
}
.color-picker::-webkit-color-swatch {
border: none;
border-radius: 0.375rem;
}
.color-value {
font-family: monospace;
font-size: 0.9375rem;
color: var(--text-secondary);
}
/* Image upload in popup */
.image-upload-container {
display: flex;
flex-direction: column;
gap: 1rem;
align-items: flex-start;
}
.image-preview {
width: 6rem;
height: 6rem;
border-radius: 0.375rem;
overflow: hidden;
background-color: var(--bg-tertiary);
position: relative;
}
.image-preview img {
width: 100%;
height: 100%;
object-fit: cover;
}
.upload-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: flex;
align-items: center;
justify-content: center;
background-color: rgba(0, 0, 0, 0.2);
color: white;
}
.popup-actions {
display: flex;
justify-content: flex-end;
gap: 0.75rem;
margin-top: 1.5rem;
}
/* Responsive design */
@media (max-width: 768px) {
.teams-grid {
grid-template-columns: 1fr;
}
.content-header {
flex-direction: column;
align-items: flex-start;
gap: 1rem;
}
.actions {
width: 100%;
}
.action-button {
flex: 1;
justify-content: center;
}
}
</style>

View file

@ -0,0 +1,6 @@
export function load({ params }: any){
return {
tournamentGuid: params.tournamentguid,
params: params
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

View file

@ -0,0 +1,86 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg id="Layer_2" data-name="Layer 2" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 3100.34 342.9">
<defs>
<style>
.cls-1 {
fill: #febf3a;
}
.cls-2 {
fill: url(#linear-gradient-2);
}
.cls-3 {
fill: none;
}
.cls-4 {
fill: url(#linear-gradient-3);
}
.cls-5 {
fill: url(#linear-gradient);
}
</style>
<linearGradient id="linear-gradient" x1="0" y1="-607.2" x2="373.81" y2="-607.2" gradientTransform="translate(0 -435.75) scale(1 -1)" gradientUnits="userSpaceOnUse">
<stop offset="0" stop-color="#d07e3c"/>
<stop offset="1" stop-color="#fec03c"/>
</linearGradient>
<linearGradient id="linear-gradient-2" x1="41.57" y1="-604.44" x2="332.23" y2="-604.44" gradientTransform="translate(0 -435.75) scale(1 -1)" gradientUnits="userSpaceOnUse">
<stop offset="0" stop-color="#5469b1"/>
<stop offset="1" stop-color="#02a9c5"/>
</linearGradient>
<linearGradient id="linear-gradient-3" x1="83.94" y1="-591.33" x2="290.24" y2="-591.33" xlink:href="#linear-gradient"/>
</defs>
<g id="Layer_2-2" data-name="Layer 2">
<g>
<g id="Layer_1-2" data-name="Layer 1-2">
<g>
<g>
<path class="cls-3" d="m224.43,255.28c.48-.19.96-.36,1.43-.56-.47.2-.95.39-1.43.57h0Z"/>
<path class="cls-3" d="m256.98,234.93c1.32-1.22,2.61-2.47,3.86-3.74-1.26,1.28-2.54,2.53-3.86,3.74Z"/>
<g>
<path class="cls-1" d="m562.77,240.31c-.74-.74-1.11-1.58-1.11-2.52V104.91c0-1.07.37-1.95,1.11-2.62.74-.67,1.58-1.01,2.52-1.01h90.34c1.07,0,1.95.34,2.62,1.01.67.67,1.01,1.55,1.01,2.62v16.94c0,.94-.34,1.78-1.01,2.52s-1.55,1.11-2.62,1.11h-65.13c-.94,0-1.78.34-2.52,1.01-.74.67-1.11,1.55-1.11,2.62v30.25c0,1.21.27,2.05.81,2.52s1.81.71,3.83.71h50.41c.94,0,1.75.3,2.42.91.67.6,1.01,1.38,1.01,2.32v17.14c0,.94-.34,1.71-1.01,2.32-.67.6-1.48.91-2.42.91h-50.61c-1.75,0-2.92.5-3.53,1.51-.6,1.01-.91,2.05-.91,3.13v21.37c0,1.35.37,2.49,1.11,3.43.74.94,2.25,1.41,4.54,1.41h64.53c.94,0,1.78.37,2.52,1.11s1.11,1.65,1.11,2.72v16.94c0,.94-.34,1.78-1.01,2.52s-1.55,1.11-2.62,1.11h-91.75c-.94,0-1.78-.37-2.52-1.11v-.02Z"/>
<path class="cls-1" d="m677.94,241.42c-.4-.54-.54-1.14-.4-1.81l35.69-135.71c.27-.81.77-1.51,1.51-2.12.74-.6,1.58-.91,2.52-.91h41.94c.81,0,1.61.27,2.42.81s1.34,1.21,1.61,2.02l35.69,135.71v.6c0,1.48-.67,2.22-2.02,2.22h-19.76c-.81,0-1.65-.3-2.52-.91-.88-.6-1.38-1.31-1.51-2.12l-9.88-37.51c-.27-1.21-.94-2.22-2.02-3.02-1.08-.81-2.22-1.21-3.43-1.21h-39.12c-1.21,0-2.35.4-3.43,1.21-1.08.81-1.75,1.81-2.02,3.02l-9.88,37.3c-.14.94-.64,1.71-1.51,2.32-.88.6-1.71.91-2.52.91h-19.76c-.67,0-1.21-.27-1.61-.81h.01Zm73.6-65.54c1.34,0,2.38-.47,3.13-1.41.74-.94.97-2.02.71-3.23l-11.29-41.54c-.14-1.07-.74-2.02-1.81-2.82-1.08-.81-2.22-1.21-3.43-1.21h-1.21c-1.21,0-2.35.4-3.43,1.21s-1.68,1.75-1.81,2.82l-11.09,41.54-.2,1.01c0,1.08.33,1.95,1.01,2.62.67.67,1.61,1.01,2.82,1.01h26.62-.02Z"/>
<path class="cls-1" d="m835.97,241.22c-.47-.54-.71-1.14-.71-1.81v-109.29c0-1.21-.5-2.32-1.51-3.33s-2.19-1.51-3.53-1.51h-33.88c-.54,0-1.08-.27-1.61-.81-.54-.54-.81-1.07-.81-1.61v-19.56c0-.67.23-1.21.71-1.61.47-.4,1.04-.6,1.71-.6h106.47c.81,0,1.48.27,2.02.81.54.54.81,1.14.81,1.81v18.95c0,.67-.27,1.28-.81,1.81-.54.54-1.21.81-2.02.81h-33.47c-1.35,0-2.49.47-3.43,1.41s-1.41,2.09-1.41,3.43v109.29c0,.67-.24,1.28-.71,1.81-.47.54-1.11.81-1.92.81h-24c-.81,0-1.45-.27-1.92-.81h.02Z"/>
<path class="cls-1" d="m925.6,240.92c-.74-.74-1.11-1.58-1.11-2.52V104.5c0-1.07.37-1.98,1.11-2.72s1.58-1.11,2.52-1.11h21.78c.94,0,1.78.37,2.52,1.11s1.11,1.65,1.11,2.72v46.38l40.93-47.99c1.21-1.48,2.69-2.22,4.44-2.22h28.84c1.48,0,2.35.27,2.62.81s.2,1.08-.2,1.61l-73.4,85.9c-2.15,2.56-3.23,5.78-3.23,9.68v39.72c0,.94-.34,1.78-1.01,2.52s-1.55,1.11-2.62,1.11h-21.78c-.94,0-1.78-.37-2.52-1.11h0Zm73.8,0c-1.28-.6-2.32-1.51-3.13-2.72l-21.37-30.45c-.94-1.48-1.41-2.69-1.41-3.63,0-1.21.54-2.49,1.61-3.83,1.07-1.21,2.32-2.65,3.73-4.34,1.41-1.68,2.79-3.26,4.13-4.74l4.23-5.04c.81-1.07,1.75-1.61,2.82-1.61,1.21,0,2.22.6,3.02,1.81l36.3,51.42c.27.54.4,1.01.4,1.41,0,1.75-1.21,2.69-3.63,2.82h-20.57c-2.82-.13-4.87-.5-6.15-1.11h.02Z"/>
<path class="cls-1" d="m1052.27,241.22c-.54-.54-.81-1.21-.81-2.02V103.5c0-.81.27-1.48.81-2.02s1.21-.81,2.02-.81h21.17c.81,0,1.48.3,2.02.91.54.6.81,1.24.81,1.92v52.63c0,1.21.47,2.29,1.41,3.23.94.94,2.02,1.41,3.23,1.41h38.31c1.21,0,2.25-.44,3.13-1.31s1.38-1.98,1.51-3.33v-52.63c0-.67.27-1.31.81-1.92.54-.6,1.21-.91,2.02-.91h21.17c.81,0,1.48.27,2.02.81s.81,1.21.81,2.02v135.71c0,.81-.27,1.48-.81,2.02-.54.54-1.21.81-2.02.81h-21.17c-.81,0-1.48-.3-2.02-.91-.54-.6-.81-1.24-.81-1.92v-46.78c-.14-1.34-.64-2.45-1.51-3.33-.88-.87-1.92-1.31-3.13-1.31h-38.31c-1.21,0-2.25.4-3.13,1.21-.88.81-1.38,1.88-1.51,3.23v46.98c0,.67-.27,1.31-.81,1.92-.54.6-1.21.91-2.02.91h-21.17c-.81,0-1.48-.27-2.02-.81h0Z"/>
<path class="cls-1" d="m1172.38,241.42c-.4-.54-.54-1.14-.4-1.81l35.69-135.71c.27-.81.77-1.51,1.51-2.12.74-.6,1.58-.91,2.52-.91h41.94c.81,0,1.61.27,2.42.81s1.34,1.21,1.61,2.02l35.69,135.71v.6c0,1.48-.67,2.22-2.02,2.22h-19.76c-.81,0-1.65-.3-2.52-.91-.88-.6-1.38-1.31-1.51-2.12l-9.88-37.51c-.27-1.21-.94-2.22-2.02-3.02-1.08-.81-2.22-1.21-3.43-1.21h-39.12c-1.21,0-2.35.4-3.43,1.21-1.08.81-1.75,1.81-2.02,3.02l-9.88,37.3c-.14.94-.64,1.71-1.51,2.32-.88.6-1.71.91-2.52.91h-19.76c-.67,0-1.21-.27-1.61-.81h.01Zm73.6-65.54c1.34,0,2.38-.47,3.13-1.41.74-.94.97-2.02.71-3.23l-11.29-41.54c-.14-1.07-.74-2.02-1.81-2.82-1.08-.81-2.22-1.21-3.43-1.21h-1.21c-1.21,0-2.35.4-3.43,1.21-1.08.81-1.68,1.75-1.81,2.82l-11.09,41.54-.2,1.01c0,1.08.33,1.95,1.01,2.62.67.67,1.61,1.01,2.82,1.01h26.62-.02Z"/>
<path class="cls-1" d="m1313.42,240.82c-.6-.54-.91-1.21-.91-2.02V103.9c0-.81.3-1.48.91-2.02.6-.54,1.31-.81,2.12-.81h20.57c.94,0,1.68.47,2.22,1.41l47.79,80.86c1.48,2.69,2.69,4.03,3.63,4.03,1.07,0,1.61-1.48,1.61-4.44l-.4-79.04c0-.81.3-1.48.91-2.02.6-.54,1.31-.81,2.12-.81h21.17c.81,0,1.51.27,2.12.81.6.54.91,1.21.91,2.02v134.9c0,.81-.3,1.48-.91,2.02-.6.54-1.31.81-2.12.81h-14.32c-2.42,0-4.1-.2-5.04-.6-.94-.4-1.95-1.48-3.02-3.23l-50.81-80.86c-.81-1.21-1.55-1.81-2.22-1.81-1.08,0-1.61.94-1.61,2.82l.4,80.86c0,.81-.3,1.48-.91,2.02-.6.54-1.24.81-1.92.81h-20.17c-.81,0-1.51-.27-2.12-.81h0Z"/>
<path class="cls-1" d="m1437.78,241.42c-.4-.54-.54-1.14-.4-1.81l35.69-135.71c.27-.81.77-1.51,1.51-2.12.74-.6,1.58-.91,2.52-.91h41.94c.81,0,1.61.27,2.42.81s1.34,1.21,1.61,2.02l35.69,135.71v.6c0,1.48-.67,2.22-2.02,2.22h-19.76c-.81,0-1.65-.3-2.52-.91-.88-.6-1.38-1.31-1.51-2.12l-9.88-37.51c-.27-1.21-.94-2.22-2.02-3.02-1.08-.81-2.22-1.21-3.43-1.21h-39.12c-1.21,0-2.35.4-3.43,1.21-1.08.81-1.75,1.81-2.02,3.02l-9.88,37.3c-.14.94-.64,1.71-1.51,2.32-.88.6-1.71.91-2.52.91h-19.76c-.67,0-1.21-.27-1.61-.81h.01Zm73.6-65.54c1.34,0,2.38-.47,3.13-1.41.74-.94.97-2.02.71-3.23l-11.29-41.54c-.14-1.07-.74-2.02-1.81-2.82-1.08-.81-2.22-1.21-3.43-1.21h-1.21c-1.21,0-2.35.4-3.43,1.21-1.08.81-1.68,1.75-1.81,2.82l-11.09,41.54-.2,1.01c0,1.08.33,1.95,1.01,2.62.67.67,1.61,1.01,2.82,1.01h26.62-.02Z"/>
<path class="cls-1" d="m1582.47,237.39c-3.09-3.09-4.64-6.72-4.64-10.89s1.51-8,4.54-11.09c3.02-3.09,6.69-4.64,10.99-4.64s8,1.55,11.09,4.64,4.64,6.79,4.64,11.09-1.55,7.97-4.64,10.99-6.79,4.54-11.09,4.54-7.8-1.54-10.89-4.64Zm0-35.89c-.81-.81-1.28-1.81-1.41-3.02l-1.61-92.35c0-1.21.4-2.22,1.21-3.02s1.81-1.21,3.02-1.21h19.56c1.21,0,2.22.4,3.02,1.21s1.21,1.81,1.21,3.02l-1.61,92.35c-.14,1.21-.6,2.22-1.41,3.02-.81.81-1.81,1.21-3.02,1.21h-15.93c-1.21,0-2.22-.4-3.02-1.21h0Z"/>
<path class="cls-1" d="m468.39,240.81v-20.83c0-.94.47-1.42,1.42-1.42h33.37c4.85,0,8.93-1.75,12.23-5.26,3.3-3.5,4.95-7.68,4.95-12.54s-1.69-8.93-5.06-12.23-7.42-4.95-12.13-4.95h-25.48c-1.35,0-2.02-.67-2.02-2.02v-21.03c0-.27.13-.57.4-.91.27-.33.61-.51,1.01-.51h11.32c4.58,0,8.52-1.68,11.83-5.06,3.3-3.37,4.95-7.35,4.95-11.93s-1.69-8.76-5.06-12.13c-3.37-3.37-7.42-5.06-12.13-5.06h-14.56c-1.89,0-3.54.68-4.95,2.02-1.42,1.35-2.12,2.97-2.12,4.85v5.26c0,.4-.17.74-.51,1.01-.34.27-.64.4-.91.4h-17.59c-1.08,0-1.62-.47-1.62-1.42v-34.98c0-.94.54-1.42,1.62-1.42h40.65c7.55,0,14.49,1.85,20.83,5.56,6.34,3.71,11.36,8.73,15.07,15.07s5.56,13.28,5.56,20.83-2.16,15.17-6.47,22.04c6.74,3.78,12.06,8.93,15.98,15.47,3.91,6.54,5.86,13.58,5.86,21.13s-1.85,14.49-5.56,20.83-8.77,11.36-15.17,15.07c-6.4,3.71-13.38,5.56-20.93,5.56h-33.37c-.94,0-1.42-.47-1.42-1.42v.02Z"/>
</g>
</g>
<g>
<path class="cls-5" d="m40.5,225.67l-9.21,5.32c-6.89,3.98-15.66.87-18.59-6.52-7.01-17.65-11.4-36.61-12.7-56.4h13.45c6.42,0,11.9,4.69,12.83,11.05,2.4,16.44,7.27,32.09,14.23,46.55h0Zm-10.06-146l-11.64-6.72c-8.47,17.15-14.39,35.77-17.25,55.38-1.14,7.79,5,14.74,12.87,14.74h10.63c1.27-16.61,5.06-32.53,10.97-47.37,2.38-5.98,0-12.8-5.57-16.02h-.01ZM89.39,11.67l-6.74-11.67c-16.07,10.8-30.39,24.03-42.41,39.15-4.96,6.24-3.29,15.41,3.61,19.4l9.18,5.3c9.24-13.44,20.45-25.43,33.21-35.55,5.03-3.99,6.36-11.07,3.15-16.63Zm244.17,27.48c-12.03-15.12-26.34-28.35-42.41-39.15l-6.74,11.67c-3.21,5.56-1.88,12.65,3.15,16.63,12.76,10.11,23.97,22.1,33.21,35.55l9.18-5.3c6.9-3.99,8.58-13.16,3.61-19.4h0Zm38.69,89.18c-2.87-19.6-8.78-38.23-17.26-55.38l-11.64,6.72c-5.58,3.22-7.95,10.04-5.57,16.02,5.92,14.85,9.7,30.76,10.97,47.37h10.63c7.87,0,14-6.96,12.86-14.74h.01Zm-11.89,39.74c-6.42,0-11.9,4.69-12.83,11.05-2.4,16.44-7.27,32.09-14.23,46.55l9.21,5.32c6.89,3.98,15.66.87,18.59-6.52,7.01-17.65,11.4-36.61,12.71-56.4h-13.46.01Zm-29.56,85.02c-5.56-3.21-12.64-1.88-16.63,3.15-10.11,12.76-22.1,23.96-35.55,33.2-6.85,4.71-14.08,8.91-21.63,12.54-17.67,8.49-37.1,13.88-57.59,15.45-4.13.31-8.29.47-12.5.47s-8.37-.16-12.5-.47c-20.49-1.57-39.92-6.96-57.59-15.45-7.55-3.63-14.78-7.83-21.63-12.54-13.44-9.24-25.43-20.44-35.55-33.2-3.99-5.03-11.07-6.36-16.63-3.15l-11.67,6.74c13.62,20.24,31.08,37.7,51.32,51.31,6.9,4.64,14.12,8.83,21.63,12.54,21.4,10.56,45.09,17.15,70.12,18.81,4.13.27,8.3.41,12.5.41s8.37-.14,12.5-.41c25.03-1.66,48.72-8.25,70.12-18.81,7.51-3.71,14.73-7.9,21.63-12.54,20.24-13.61,37.7-31.07,51.32-51.31l-11.67-6.74h0Z"/>
<path class="cls-2" d="m332.23,155.57c0,4.21-.18,8.38-.54,12.5-1.48,17.4-6.05,33.94-13.16,49.07-3.56,7.59-7.76,14.82-12.54,21.63-9.77,13.96-21.94,26.13-35.9,35.9-6.81,4.77-14.04,8.98-21.63,12.54-15.12,7.1-31.66,11.67-49.06,13.15-4.12.36-8.29.54-12.5.54s-8.38-.18-12.5-.54c-17.4-1.48-33.94-6.05-49.06-13.15-7.59-3.56-14.82-7.77-21.63-12.54-13.96-9.77-26.13-21.94-35.9-35.9-4.78-6.81-8.98-14.04-12.54-21.63-7.11-15.13-11.68-31.67-13.16-49.07-.36-4.12-.54-8.29-.54-12.5s.18-8.38.54-12.5c1.48-17.4,6.05-33.94,13.16-49.06,3.56-7.59,7.76-14.82,12.53-21.63,9.78-13.96,21.95-26.13,35.91-35.9l6.97,12.07c3.13,5.42,1.9,12.25-2.81,16.36-6.83,5.96-12.98,12.68-18.32,20.02-4.9,6.72-9.11,13.96-12.53,21.63-5.09,11.35-8.47,23.63-9.8,36.51-.43,4.11-.65,8.28-.65,12.5s.22,8.39.65,12.5c1.33,12.88,4.71,25.16,9.8,36.51,3.42,7.67,7.63,14.91,12.53,21.63,7.44,10.24,16.47,19.27,26.72,26.71,6.72,4.91,13.96,9.12,21.63,12.54,11.35,5.09,23.63,8.46,36.5,9.79,4.11.43,8.28.65,12.5.65s8.39-.22,12.5-.65c12.87-1.33,25.15-4.7,36.5-9.79,7.67-3.42,14.91-7.63,21.63-12.54,10.25-7.44,19.28-16.47,26.72-26.71,4.9-6.72,9.11-13.96,12.53-21.63,5.09-11.35,8.47-23.63,9.8-36.51.43-4.11.65-8.28.65-12.5s-.22-8.39-.65-12.5c-1.33-12.88-4.71-25.16-9.8-36.51-3.42-7.67-7.63-14.91-12.53-21.63-5.33-7.34-11.49-14.06-18.32-20.02-4.71-4.11-5.94-10.94-2.81-16.36l6.97-12.07c13.96,9.77,26.13,21.94,35.91,35.9,4.77,6.81,8.97,14.04,12.53,21.63,7.11,15.12,11.68,31.66,13.16,49.06.36,4.12.54,8.29.54,12.5h0Z"/>
<path class="cls-4" d="m290.23,155.57c.52,41.68-26.82,81.58-65.8,96.29v-23.19c0-2.08-1.69-3.77-3.77-3.77h-17.45c-2.08,0-3.77,1.69-3.77,3.77v29.47c-8.3,1.01-16.75,1.02-25.05,0v-124.47c0-2.08-1.69-3.77-3.77-3.77h-17.45c-2.08,0-3.77,1.69-3.77,3.77v118.2c-87.29-34.19-87.26-158.42,0-192.6v43.94c0,2.08,1.69,3.77,3.77,3.77h17.45c2.08,0,3.77-1.69,3.77-3.77v-50.2c8.18-1.01,16.87-1.01,25.05,0v145.2c0,2.08,1.69,3.77,3.77,3.77h17.45c2.08,0,3.77-1.69,3.77-3.77V59.27c39.02,14.78,66.3,54.58,65.8,96.3Z"/>
<path class="cls-3" d="m224.43,255.28c.48-.19.96-.36,1.43-.56-.47.2-.95.39-1.43.57h0Z"/>
<path class="cls-3" d="m256.98,234.93c1.32-1.22,2.61-2.47,3.86-3.74-1.26,1.28-2.54,2.53-3.86,3.74Z"/>
</g>
</g>
</g>
<g>
<path d="m1813.51,111.36c-10.83-6.33-22.65-9.5-35.45-9.5h-40.87c-1.23,0-2.25.44-3.07,1.33-.82.89-1.23,1.94-1.23,3.17v131.18c0,1.63.58,3.03,1.74,4.19,1.16,1.16,2.55,1.74,4.19,1.74h39.23c12.8,0,24.62-3.17,35.45-9.5,10.83-6.33,19.41-14.92,25.75-25.75,6.33-10.83,9.5-22.65,9.5-35.45s-3.17-24.65-9.5-35.55c-6.33-10.9-14.92-19.51-25.75-25.85Zm3.07,83.67c-4.02,6.81-9.43,12.19-16.24,16.14-6.81,3.95-14.24,5.93-22.27,5.93h-18.8v-86.43c0-.68.24-1.26.71-1.74.48-.48,1.05-.71,1.74-.71h16.35c8.04,0,15.46,2.01,22.27,6.03,6.81,4.02,12.22,9.43,16.24,16.24,4.02,6.81,6.03,14.24,6.03,22.27s-2.01,15.46-6.03,22.27Z"/>
<path d="m1868.41,126.38h94.81c1.09,0,2.01-.41,2.76-1.23.75-.82,1.12-1.7,1.12-2.66v-17.57c0-.95-.38-1.8-1.12-2.55-.75-.75-1.67-1.12-2.76-1.12h-94.81c-.96,0-1.81.34-2.55,1.02-.75.68-1.12,1.57-1.12,2.66v17.57c0,1.09.37,2.01,1.12,2.76.75.75,1.6,1.12,2.55,1.12Z"/>
<path d="m1963.63,219.35h-70.29c-.95,0-1.84-.41-2.66-1.23s-1.23-1.7-1.23-2.66v-22.27c0-2.86,1.56-4.29,4.7-4.29h52.11c.95,0,1.77-.31,2.45-.92.68-.61,1.02-1.39,1.02-2.35v-17.57c0-.95-.34-1.74-1.02-2.35-.68-.61-1.5-.92-2.45-.92h-51.49c-3.54,0-5.31-1.29-5.31-3.88v-14.71c0-1.09-.34-1.97-1.02-2.66-.68-.68-1.57-1.02-2.66-1.02h-17.37c-.96,0-1.81.34-2.55,1.02-.75.68-1.12,1.57-1.12,2.66v94.4c0,.95.37,1.81,1.12,2.55.75.75,1.6,1.12,2.55,1.12h95.22c1.09,0,1.97-.37,2.66-1.12.68-.75,1.02-1.6,1.02-2.55v-17.57c0-1.09-.34-1.97-1.02-2.66-.68-.68-1.57-1.02-2.66-1.02Z"/>
<path d="m2115.9,126.38h94.81c1.09,0,2.01-.41,2.76-1.23.75-.82,1.12-1.7,1.12-2.66v-17.57c0-.95-.38-1.8-1.12-2.55-.75-.75-1.67-1.12-2.76-1.12h-94.81c-.96,0-1.81.34-2.55,1.02-.75.68-1.12,1.57-1.12,2.66v17.57c0,1.09.37,2.01,1.12,2.76.75.75,1.6,1.12,2.55,1.12Z"/>
<path d="m2211.12,219.35h-70.29c-.96,0-1.84-.41-2.66-1.23s-1.23-1.7-1.23-2.66v-22.27c0-2.86,1.56-4.29,4.7-4.29h52.1c.95,0,1.77-.31,2.45-.92.68-.61,1.02-1.39,1.02-2.35v-17.57c0-.95-.34-1.74-1.02-2.35-.68-.61-1.5-.92-2.45-.92h-51.49c-3.54,0-5.31-1.29-5.31-3.88v-14.71c0-1.09-.34-1.97-1.02-2.66-.68-.68-1.57-1.02-2.66-1.02h-17.37c-.96,0-1.81.34-2.55,1.02-.75.68-1.12,1.57-1.12,2.66v94.4c0,.95.37,1.81,1.12,2.55.75.75,1.6,1.12,2.55,1.12h95.22c1.09,0,1.97-.37,2.66-1.12.68-.75,1.02-1.6,1.02-2.55v-17.57c0-1.09-.34-1.97-1.02-2.66-.68-.68-1.57-1.02-2.66-1.02Z"/>
<path d="m2323.42,218.33h-49.45c-1.09,0-2.04-.44-2.86-1.33-.82-.88-1.23-1.87-1.23-2.96v-108.7c0-1.09-.41-2.08-1.23-2.96-.82-.88-1.84-1.33-3.06-1.33h-22.07c-1.09,0-2.08.44-2.96,1.33-.89.89-1.33,1.87-1.33,2.96v134.65c0,1.23.44,2.25,1.33,3.07.88.82,1.87,1.23,2.96,1.23h79.89c1.23,0,2.25-.41,3.06-1.23s1.23-1.84,1.23-3.07v-17.57c0-1.09-.41-2.04-1.23-2.86s-1.84-1.23-3.06-1.23Z"/>
<path d="m2464.73,132.71c-.96,0-1.98.34-3.07,1.02-.82.55-2.79,1.91-5.93,4.09-3.13,2.18-5.25,3.61-6.33,4.29-1.63,1.36-2.45,2.93-2.45,4.7,0,1.36.34,2.73,1.02,4.09,3.4,6.68,5.11,13.89,5.11,21.66,0,5.45-.92,10.8-2.76,16.04-1.84,5.25-4.6,10.05-8.28,14.41-4.36,5.45-9.67,9.6-15.94,12.46-6.27,2.86-12.81,4.29-19.62,4.29-11.17,0-21.25-3.68-30.24-11.03-5.31-4.5-9.4-9.91-12.26-16.24-2.86-6.33-4.29-12.91-4.29-19.72,0-11.31,3.68-21.45,11.03-30.45,3.81-4.9,8.51-8.82,14.1-11.75,5.58-2.93,11.44-4.66,17.57-5.21,2.86-.27,4.29-1.7,4.29-4.29v-17.78c-.14-1.23-.58-2.25-1.33-3.06-.75-.82-1.67-1.23-2.76-1.23h-.41c-9.95.55-19.41,3.03-28.4,7.46-8.99,4.43-16.83,10.73-23.5,18.9-5.59,6.68-9.78,14.07-12.57,22.17-2.79,8.11-4.19,16.38-4.19,24.83,0,10.76,2.25,21.15,6.74,31.16,4.5,10.01,10.96,18.63,19.41,25.85,6.26,5.59,13.42,9.84,21.46,12.77,8.04,2.93,16.35,4.39,24.93,4.39,10.9,0,21.35-2.28,31.37-6.85,10.01-4.56,18.35-11.14,25.03-19.72,5.58-6.67,9.81-14.06,12.67-22.17,2.86-8.1,4.29-16.45,4.29-25.03,0-13.21-3.27-25.54-9.81-36.98-1.09-2.04-2.73-3.06-4.9-3.06Z"/>
<path d="m2572.09,107.17c-6.95-4.09-14.51-6.13-22.68-6.13h-45.16c-.95,0-1.81.38-2.55,1.12-.75.75-1.12,1.67-1.12,2.76v18.8c0,1.09.37,2.01,1.12,2.76.75.75,1.6,1.12,2.55,1.12h45.16c5.18,0,9.64,1.84,13.38,5.52,3.75,3.68,5.62,8.17,5.62,13.49s-1.87,9.6-5.62,13.28c-3.75,3.68-8.21,5.52-13.38,5.52h-45.16c-.95,0-1.81.38-2.55,1.12-.75.75-1.12,1.67-1.12,2.76v71.31c0,.95.37,1.81,1.12,2.55.75.75,1.6,1.12,2.55,1.12h15.73c1.09,0,2.01-.34,2.76-1.02.75-.68,1.12-1.56,1.12-2.66v-45.57c0-1.23.27-2.04.82-2.45.54-.41,1.56-.61,3.06-.61h21.66c8.17,0,15.73-2.04,22.68-6.13,6.95-4.09,12.46-9.6,16.55-16.55,4.09-6.95,6.13-14.51,6.13-22.68s-2.04-15.77-6.13-22.78c-4.09-7.01-9.6-12.57-16.55-16.65Z"/>
<path d="m2735.24,101.66h-20.64c-.68,0-1.23.24-1.63.72-.41.48-.82,1.06-1.23,1.74l-30.85,50.67c-.14.27-.48.75-1.02,1.43-.55.68-1.3,1.02-2.25,1.02h-.2c-.96,0-1.74-.27-2.35-.82-.61-.54-.99-.95-1.12-1.23l-31.06-51.29c-.27-.54-.71-1.05-1.33-1.53-.61-.48-1.12-.72-1.53-.72h-20.43c-.96,0-1.81.38-2.55,1.12s-1.12,1.67-1.12,2.76v135.35c0,1.76,1.43,3.19,3.19,3.19h20.8c1.76,0,3.19-1.43,3.19-3.19v-81.61c0-1.23.27-1.84.82-1.84.68,0,1.29.61,1.84,1.84l22.27,31.26c1.09,1.36,2.08,2.25,2.96,2.66.88.41,2.55.61,5.01.61h3.47c2.04,0,3.51-.27,4.39-.82.88-.54,2.01-1.7,3.37-3.47,2.31-3.13,5.96-8.17,10.93-15.12,4.97-6.95,8.55-12.06,10.73-15.32.82-.95,1.43-1.43,1.84-1.43.54,0,.82.61.82,1.84v80.92c0,1.09.34,1.98,1.02,2.66.68.68,1.5,1.02,2.45,1.02h20.23c.95,0,1.77-.34,2.45-1.02s1.02-1.56,1.02-2.66V105.54c0-1.09-.34-2.01-1.02-2.76-.68-.75-1.5-1.12-2.45-1.12Z"/>
<path d="m2771.32,126.38h94.81c1.09,0,2.01-.41,2.76-1.23.75-.82,1.12-1.7,1.12-2.66v-17.57c0-.95-.38-1.8-1.12-2.55-.75-.75-1.67-1.12-2.76-1.12h-94.81c-.96,0-1.81.34-2.55,1.02-.75.68-1.12,1.57-1.12,2.66v17.57c0,1.09.37,2.01,1.12,2.76.75.75,1.6,1.12,2.55,1.12Z"/>
<path d="m2866.54,219.35h-70.29c-.96,0-1.84-.41-2.66-1.23s-1.23-1.7-1.23-2.66v-22.27c0-2.86,1.56-4.29,4.7-4.29h52.1c.95,0,1.77-.31,2.45-.92.68-.61,1.02-1.39,1.02-2.35v-17.57c0-.95-.34-1.74-1.02-2.35-.68-.61-1.5-.92-2.45-.92h-51.49c-3.54,0-5.31-1.29-5.31-3.88v-14.71c0-1.09-.34-1.97-1.02-2.66-.68-.68-1.57-1.02-2.66-1.02h-17.37c-.96,0-1.81.34-2.55,1.02-.75.68-1.12,1.57-1.12,2.66v94.4c0,.95.37,1.81,1.12,2.55.75.75,1.6,1.12,2.55,1.12h95.22c1.09,0,1.97-.37,2.66-1.12.68-.75,1.02-1.6,1.02-2.55v-17.57c0-1.09-.34-1.97-1.02-2.66-.68-.68-1.57-1.02-2.66-1.02Z"/>
<path d="m2985.58,101.45h-21.66c-.95,0-1.81.38-2.55,1.12-.75.75-1.12,1.67-1.12,2.76l.2,79.69c0,2.32-.34,3.47-1.02,3.47-.82,0-1.84-1.16-3.07-3.47l-49.45-82.35c-.68-.82-1.5-1.23-2.45-1.23h-20.02c-1.09,0-2.01.38-2.76,1.12-.75.75-1.12,1.67-1.12,2.76v78.06c0,1.09.37,1.98,1.12,2.66.75.68,1.67,1.02,2.76,1.02h19.82c1.09,0,1.97-.37,2.66-1.12.68-.75,1.02-1.6,1.02-2.55l-.41-24.11c0-.27.03-.68.1-1.23.07-.54.24-.82.51-.82s.82.48,1.63,1.43c6.13,9.81,15.22,24.04,27.28,42.71,12.06,18.67,20.74,32.15,26.05,40.46,1.09,1.36,2.18,2.04,3.27,2.04h19.21c1.09,0,2.01-.34,2.76-1.02.75-.68,1.12-1.56,1.12-2.66V105.33c0-1.09-.38-2.01-1.12-2.76-.75-.75-1.67-1.12-2.76-1.12Z"/>
<path d="m3099.53,102.27c-.55-.54-1.16-.82-1.84-.82h-89.29c-.68,0-1.3.27-1.84.82s-.82,1.23-.82,2.04v19c0,.82.27,1.5.82,2.04.54.55,1.16.82,1.84.82h11.65c1.36,0,2.52.44,3.47,1.33.95.89,1.43,2.01,1.43,3.37v110.34c0,1.77.88,2.66,2.66,2.66h24.32c.68,0,1.29-.24,1.84-.72.54-.48.82-1.12.82-1.94v-110.34c0-1.23.48-2.31,1.43-3.27.95-.95,2.11-1.43,3.47-1.43h38.21c.68,0,1.29-.27,1.84-.82s.82-1.23.82-2.04v-19c0-.82-.27-1.5-.82-2.04Z"/>
<path d="m2016.21,121.54l-4.86-17.82c-.41-1.52-1.79-2.57-3.36-2.57l-19.28.03c-2.13,0-3.68,2.03-3.12,4.09l4.98,18.28c.45,1.67,1.97,2.83,3.7,2.83h18.24c2.53,0,4.36-2.4,3.7-4.84Z"/>
<path d="m2078.75,142.52h-18.35c-1.73,0-3.25,1.16-3.7,2.83-.54,2-1.1,4.05-1.67,6.16-6,22.07-10.56,38.96-13.69,50.68-.55,2.31-1.03,3.47-1.43,3.47s-.95-1.08-1.63-3.23l-4.92-18.07-10.64-39.01c-.45-1.67-1.97-2.83-3.7-2.83h-18.25c-2.53,0-4.36,2.4-3.7,4.84l10.05,36.9h0s.09.35.09.35l15.64,57.4c.28.87.73,1.46,1.32,1.76.68.34,1.7.51,3.07.51h24.93c2.58,0,4.08-.89,4.49-2.66l24.66-90.11,1.13-4.14c.67-2.44-1.17-4.85-3.7-4.85Z"/>
<path d="m2091.18,101.04l-19.29.03c-.91.06-1.67.26-2.26.59-.75.4-1.26,1.15-1.54,2.24-1.27,4.53-2.9,10.4-4.89,17.62-.67,2.44,1.16,4.86,3.69,4.86h18.37c1.73,0,3.24-1.16,3.7-2.82l4.88-17.82c.27-1.22.13-2.31-.41-3.27-.55-.95-1.3-1.43-2.25-1.43Z"/>
<rect x="1660.44" y="189.43" width="24.93" height="24.93" rx="2.87" ry="2.87"/>
<rect x="1660.44" y="130.96" width="24.93" height="24.93" rx="2.87" ry="2.87"/>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 275 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

View file

@ -0,0 +1,90 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg id="Layer_2" data-name="Layer 2" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 3100.34 342.9">
<defs>
<style>
.cls-1 {
fill: #febf3a;
}
.cls-2 {
fill: url(#linear-gradient-2);
}
.cls-3 {
fill: #fff;
}
.cls-4 {
fill: none;
}
.cls-5 {
fill: url(#linear-gradient-3);
}
.cls-6 {
fill: url(#linear-gradient);
}
</style>
<linearGradient id="linear-gradient" x1="0" y1="172.55" x2="373.81" y2="172.55" gradientTransform="translate(0 344) scale(1 -1)" gradientUnits="userSpaceOnUse">
<stop offset="0" stop-color="#d07e3c"/>
<stop offset="1" stop-color="#fec03c"/>
</linearGradient>
<linearGradient id="linear-gradient-2" x1="41.57" y1="175.31" x2="332.23" y2="175.31" gradientTransform="translate(0 344) scale(1 -1)" gradientUnits="userSpaceOnUse">
<stop offset="0" stop-color="#5469b1"/>
<stop offset="1" stop-color="#02a9c5"/>
</linearGradient>
<linearGradient id="linear-gradient-3" x1="83.94" y1="188.42" x2="290.24" y2="188.42" xlink:href="#linear-gradient"/>
</defs>
<g id="Layer_2-2" data-name="Layer 2">
<g>
<g id="Layer_1-2" data-name="Layer 1-2">
<g>
<g>
<path class="cls-4" d="m224.43,255.28c.48-.19.96-.36,1.43-.56-.47.2-.95.39-1.43.57h0Z"/>
<path class="cls-4" d="m256.98,234.93c1.32-1.22,2.61-2.47,3.86-3.74-1.26,1.28-2.54,2.53-3.86,3.74Z"/>
<g>
<path class="cls-1" d="m562.77,240.31c-.74-.74-1.11-1.58-1.11-2.52V104.91c0-1.07.37-1.95,1.11-2.62.74-.67,1.58-1.01,2.52-1.01h90.34c1.07,0,1.95.34,2.62,1.01.67.67,1.01,1.55,1.01,2.62v16.94c0,.94-.34,1.78-1.01,2.52s-1.55,1.11-2.62,1.11h-65.13c-.94,0-1.78.34-2.52,1.01-.74.67-1.11,1.55-1.11,2.62v30.25c0,1.21.27,2.05.81,2.52s1.81.71,3.83.71h50.41c.94,0,1.75.3,2.42.91.67.6,1.01,1.38,1.01,2.32v17.14c0,.94-.34,1.71-1.01,2.32-.67.6-1.48.91-2.42.91h-50.61c-1.75,0-2.92.5-3.53,1.51-.6,1.01-.91,2.05-.91,3.13v21.37c0,1.35.37,2.49,1.11,3.43.74.94,2.25,1.41,4.54,1.41h64.53c.94,0,1.78.37,2.52,1.11s1.11,1.65,1.11,2.72v16.94c0,.94-.34,1.78-1.01,2.52s-1.55,1.11-2.62,1.11h-91.75c-.94,0-1.78-.37-2.52-1.11v-.02Z"/>
<path class="cls-1" d="m677.94,241.42c-.4-.54-.54-1.14-.4-1.81l35.69-135.71c.27-.81.77-1.51,1.51-2.12.74-.6,1.58-.91,2.52-.91h41.94c.81,0,1.61.27,2.42.81s1.34,1.21,1.61,2.02l35.69,135.71v.6c0,1.48-.67,2.22-2.02,2.22h-19.76c-.81,0-1.65-.3-2.52-.91-.88-.6-1.38-1.31-1.51-2.12l-9.88-37.51c-.27-1.21-.94-2.22-2.02-3.02-1.08-.81-2.22-1.21-3.43-1.21h-39.12c-1.21,0-2.35.4-3.43,1.21-1.08.81-1.75,1.81-2.02,3.02l-9.88,37.3c-.14.94-.64,1.71-1.51,2.32-.88.6-1.71.91-2.52.91h-19.76c-.67,0-1.21-.27-1.61-.81h.01Zm73.6-65.54c1.34,0,2.38-.47,3.13-1.41.74-.94.97-2.02.71-3.23l-11.29-41.54c-.14-1.07-.74-2.02-1.81-2.82-1.08-.81-2.22-1.21-3.43-1.21h-1.21c-1.21,0-2.35.4-3.43,1.21s-1.68,1.75-1.81,2.82l-11.09,41.54-.2,1.01c0,1.08.33,1.95,1.01,2.62.67.67,1.61,1.01,2.82,1.01h26.62-.02Z"/>
<path class="cls-1" d="m835.97,241.22c-.47-.54-.71-1.14-.71-1.81v-109.29c0-1.21-.5-2.32-1.51-3.33s-2.19-1.51-3.53-1.51h-33.88c-.54,0-1.08-.27-1.61-.81-.54-.54-.81-1.07-.81-1.61v-19.56c0-.67.23-1.21.71-1.61.47-.4,1.04-.6,1.71-.6h106.47c.81,0,1.48.27,2.02.81.54.54.81,1.14.81,1.81v18.95c0,.67-.27,1.28-.81,1.81-.54.54-1.21.81-2.02.81h-33.47c-1.35,0-2.49.47-3.43,1.41s-1.41,2.09-1.41,3.43v109.29c0,.67-.24,1.28-.71,1.81-.47.54-1.11.81-1.92.81h-24c-.81,0-1.45-.27-1.92-.81h.02Z"/>
<path class="cls-1" d="m925.6,240.92c-.74-.74-1.11-1.58-1.11-2.52V104.5c0-1.07.37-1.98,1.11-2.72s1.58-1.11,2.52-1.11h21.78c.94,0,1.78.37,2.52,1.11s1.11,1.65,1.11,2.72v46.38l40.93-47.99c1.21-1.48,2.69-2.22,4.44-2.22h28.84c1.48,0,2.35.27,2.62.81s.2,1.08-.2,1.61l-73.4,85.9c-2.15,2.56-3.23,5.78-3.23,9.68v39.72c0,.94-.34,1.78-1.01,2.52s-1.55,1.11-2.62,1.11h-21.78c-.94,0-1.78-.37-2.52-1.11h0Zm73.8,0c-1.28-.6-2.32-1.51-3.13-2.72l-21.37-30.45c-.94-1.48-1.41-2.69-1.41-3.63,0-1.21.54-2.49,1.61-3.83,1.07-1.21,2.32-2.65,3.73-4.34,1.41-1.68,2.79-3.26,4.13-4.74l4.23-5.04c.81-1.07,1.75-1.61,2.82-1.61,1.21,0,2.22.6,3.02,1.81l36.3,51.42c.27.54.4,1.01.4,1.41,0,1.75-1.21,2.69-3.63,2.82h-20.57c-2.82-.13-4.87-.5-6.15-1.11h.02Z"/>
<path class="cls-1" d="m1052.27,241.22c-.54-.54-.81-1.21-.81-2.02V103.5c0-.81.27-1.48.81-2.02s1.21-.81,2.02-.81h21.17c.81,0,1.48.3,2.02.91.54.6.81,1.24.81,1.92v52.63c0,1.21.47,2.29,1.41,3.23.94.94,2.02,1.41,3.23,1.41h38.31c1.21,0,2.25-.44,3.13-1.31s1.38-1.98,1.51-3.33v-52.63c0-.67.27-1.31.81-1.92.54-.6,1.21-.91,2.02-.91h21.17c.81,0,1.48.27,2.02.81s.81,1.21.81,2.02v135.71c0,.81-.27,1.48-.81,2.02-.54.54-1.21.81-2.02.81h-21.17c-.81,0-1.48-.3-2.02-.91-.54-.6-.81-1.24-.81-1.92v-46.78c-.14-1.34-.64-2.45-1.51-3.33-.88-.87-1.92-1.31-3.13-1.31h-38.31c-1.21,0-2.25.4-3.13,1.21-.88.81-1.38,1.88-1.51,3.23v46.98c0,.67-.27,1.31-.81,1.92-.54.6-1.21.91-2.02.91h-21.17c-.81,0-1.48-.27-2.02-.81h0Z"/>
<path class="cls-1" d="m1172.38,241.42c-.4-.54-.54-1.14-.4-1.81l35.69-135.71c.27-.81.77-1.51,1.51-2.12.74-.6,1.58-.91,2.52-.91h41.94c.81,0,1.61.27,2.42.81s1.34,1.21,1.61,2.02l35.69,135.71v.6c0,1.48-.67,2.22-2.02,2.22h-19.76c-.81,0-1.65-.3-2.52-.91-.88-.6-1.38-1.31-1.51-2.12l-9.88-37.51c-.27-1.21-.94-2.22-2.02-3.02-1.08-.81-2.22-1.21-3.43-1.21h-39.12c-1.21,0-2.35.4-3.43,1.21-1.08.81-1.75,1.81-2.02,3.02l-9.88,37.3c-.14.94-.64,1.71-1.51,2.32-.88.6-1.71.91-2.52.91h-19.76c-.67,0-1.21-.27-1.61-.81h.01Zm73.6-65.54c1.34,0,2.38-.47,3.13-1.41.74-.94.97-2.02.71-3.23l-11.29-41.54c-.14-1.07-.74-2.02-1.81-2.82-1.08-.81-2.22-1.21-3.43-1.21h-1.21c-1.21,0-2.35.4-3.43,1.21-1.08.81-1.68,1.75-1.81,2.82l-11.09,41.54-.2,1.01c0,1.08.33,1.95,1.01,2.62.67.67,1.61,1.01,2.82,1.01h26.62-.02Z"/>
<path class="cls-1" d="m1313.42,240.82c-.6-.54-.91-1.21-.91-2.02V103.9c0-.81.3-1.48.91-2.02.6-.54,1.31-.81,2.12-.81h20.57c.94,0,1.68.47,2.22,1.41l47.79,80.86c1.48,2.69,2.69,4.03,3.63,4.03,1.07,0,1.61-1.48,1.61-4.44l-.4-79.04c0-.81.3-1.48.91-2.02.6-.54,1.31-.81,2.12-.81h21.17c.81,0,1.51.27,2.12.81.6.54.91,1.21.91,2.02v134.9c0,.81-.3,1.48-.91,2.02-.6.54-1.31.81-2.12.81h-14.32c-2.42,0-4.1-.2-5.04-.6-.94-.4-1.95-1.48-3.02-3.23l-50.81-80.86c-.81-1.21-1.55-1.81-2.22-1.81-1.08,0-1.61.94-1.61,2.82l.4,80.86c0,.81-.3,1.48-.91,2.02-.6.54-1.24.81-1.92.81h-20.17c-.81,0-1.51-.27-2.12-.81h0Z"/>
<path class="cls-1" d="m1437.78,241.42c-.4-.54-.54-1.14-.4-1.81l35.69-135.71c.27-.81.77-1.51,1.51-2.12.74-.6,1.58-.91,2.52-.91h41.94c.81,0,1.61.27,2.42.81s1.34,1.21,1.61,2.02l35.69,135.71v.6c0,1.48-.67,2.22-2.02,2.22h-19.76c-.81,0-1.65-.3-2.52-.91-.88-.6-1.38-1.31-1.51-2.12l-9.88-37.51c-.27-1.21-.94-2.22-2.02-3.02-1.08-.81-2.22-1.21-3.43-1.21h-39.12c-1.21,0-2.35.4-3.43,1.21-1.08.81-1.75,1.81-2.02,3.02l-9.88,37.3c-.14.94-.64,1.71-1.51,2.32-.88.6-1.71.91-2.52.91h-19.76c-.67,0-1.21-.27-1.61-.81h.01Zm73.6-65.54c1.34,0,2.38-.47,3.13-1.41.74-.94.97-2.02.71-3.23l-11.29-41.54c-.14-1.07-.74-2.02-1.81-2.82-1.08-.81-2.22-1.21-3.43-1.21h-1.21c-1.21,0-2.35.4-3.43,1.21-1.08.81-1.68,1.75-1.81,2.82l-11.09,41.54-.2,1.01c0,1.08.33,1.95,1.01,2.62.67.67,1.61,1.01,2.82,1.01h26.62-.02Z"/>
<path class="cls-1" d="m1582.47,237.39c-3.09-3.09-4.64-6.72-4.64-10.89s1.51-8,4.54-11.09c3.02-3.09,6.69-4.64,10.99-4.64s8,1.55,11.09,4.64,4.64,6.79,4.64,11.09-1.55,7.97-4.64,10.99-6.79,4.54-11.09,4.54-7.8-1.54-10.89-4.64Zm0-35.89c-.81-.81-1.28-1.81-1.41-3.02l-1.61-92.35c0-1.21.4-2.22,1.21-3.02s1.81-1.21,3.02-1.21h19.56c1.21,0,2.22.4,3.02,1.21s1.21,1.81,1.21,3.02l-1.61,92.35c-.14,1.21-.6,2.22-1.41,3.02-.81.81-1.81,1.21-3.02,1.21h-15.93c-1.21,0-2.22-.4-3.02-1.21h0Z"/>
<path class="cls-1" d="m468.39,240.81v-20.83c0-.94.47-1.42,1.42-1.42h33.37c4.85,0,8.93-1.75,12.23-5.26,3.3-3.5,4.95-7.68,4.95-12.54s-1.69-8.93-5.06-12.23-7.42-4.95-12.13-4.95h-25.48c-1.35,0-2.02-.67-2.02-2.02v-21.03c0-.27.13-.57.4-.91.27-.33.61-.51,1.01-.51h11.32c4.58,0,8.52-1.68,11.83-5.06,3.3-3.37,4.95-7.35,4.95-11.93s-1.69-8.76-5.06-12.13c-3.37-3.37-7.42-5.06-12.13-5.06h-14.56c-1.89,0-3.54.68-4.95,2.02-1.42,1.35-2.12,2.97-2.12,4.85v5.26c0,.4-.17.74-.51,1.01-.34.27-.64.4-.91.4h-17.59c-1.08,0-1.62-.47-1.62-1.42v-34.98c0-.94.54-1.42,1.62-1.42h40.65c7.55,0,14.49,1.85,20.83,5.56,6.34,3.71,11.36,8.73,15.07,15.07s5.56,13.28,5.56,20.83-2.16,15.17-6.47,22.04c6.74,3.78,12.06,8.93,15.98,15.47,3.91,6.54,5.86,13.58,5.86,21.13s-1.85,14.49-5.56,20.83-8.77,11.36-15.17,15.07c-6.4,3.71-13.38,5.56-20.93,5.56h-33.37c-.94,0-1.42-.47-1.42-1.42v.02Z"/>
</g>
</g>
<g>
<path class="cls-6" d="m40.5,225.67l-9.21,5.32c-6.89,3.98-15.66.87-18.59-6.52-7.01-17.65-11.4-36.61-12.7-56.4h13.45c6.42,0,11.9,4.69,12.83,11.05,2.4,16.44,7.27,32.09,14.23,46.55h0Zm-10.06-146l-11.64-6.72c-8.47,17.15-14.39,35.77-17.25,55.38-1.14,7.79,5,14.74,12.87,14.74h10.63c1.27-16.61,5.06-32.53,10.97-47.37,2.38-5.98,0-12.8-5.57-16.02h-.01ZM89.39,11.67l-6.74-11.67c-16.07,10.8-30.39,24.03-42.41,39.15-4.96,6.24-3.29,15.41,3.61,19.4l9.18,5.3c9.24-13.44,20.45-25.43,33.21-35.55,5.03-3.99,6.36-11.07,3.15-16.63Zm244.17,27.48c-12.03-15.12-26.34-28.35-42.41-39.15l-6.74,11.67c-3.21,5.56-1.88,12.65,3.15,16.63,12.76,10.11,23.97,22.1,33.21,35.55l9.18-5.3c6.9-3.99,8.58-13.16,3.61-19.4h0Zm38.69,89.18c-2.87-19.6-8.78-38.23-17.26-55.38l-11.64,6.72c-5.58,3.22-7.95,10.04-5.57,16.02,5.92,14.85,9.7,30.76,10.97,47.37h10.63c7.87,0,14-6.96,12.86-14.74h.01Zm-11.89,39.74c-6.42,0-11.9,4.69-12.83,11.05-2.4,16.44-7.27,32.09-14.23,46.55l9.21,5.32c6.89,3.98,15.66.87,18.59-6.52,7.01-17.65,11.4-36.61,12.71-56.4h-13.46.01Zm-29.56,85.02c-5.56-3.21-12.64-1.88-16.63,3.15-10.11,12.76-22.1,23.96-35.55,33.2-6.85,4.71-14.08,8.91-21.63,12.54-17.67,8.49-37.1,13.88-57.59,15.45-4.13.31-8.29.47-12.5.47s-8.37-.16-12.5-.47c-20.49-1.57-39.92-6.96-57.59-15.45-7.55-3.63-14.78-7.83-21.63-12.54-13.44-9.24-25.43-20.44-35.55-33.2-3.99-5.03-11.07-6.36-16.63-3.15l-11.67,6.74c13.62,20.24,31.08,37.7,51.32,51.31,6.9,4.64,14.12,8.83,21.63,12.54,21.4,10.56,45.09,17.15,70.12,18.81,4.13.27,8.3.41,12.5.41s8.37-.14,12.5-.41c25.03-1.66,48.72-8.25,70.12-18.81,7.51-3.71,14.73-7.9,21.63-12.54,20.24-13.61,37.7-31.07,51.32-51.31l-11.67-6.74h0Z"/>
<path class="cls-2" d="m332.23,155.57c0,4.21-.18,8.38-.54,12.5-1.48,17.4-6.05,33.94-13.16,49.07-3.56,7.59-7.76,14.82-12.54,21.63-9.77,13.96-21.94,26.13-35.9,35.9-6.81,4.77-14.04,8.98-21.63,12.54-15.12,7.1-31.66,11.67-49.06,13.15-4.12.36-8.29.54-12.5.54s-8.38-.18-12.5-.54c-17.4-1.48-33.94-6.05-49.06-13.15-7.59-3.56-14.82-7.77-21.63-12.54-13.96-9.77-26.13-21.94-35.9-35.9-4.78-6.81-8.98-14.04-12.54-21.63-7.11-15.13-11.68-31.67-13.16-49.07-.36-4.12-.54-8.29-.54-12.5s.18-8.38.54-12.5c1.48-17.4,6.05-33.94,13.16-49.06,3.56-7.59,7.76-14.82,12.53-21.63,9.78-13.96,21.95-26.13,35.91-35.9l6.97,12.07c3.13,5.42,1.9,12.25-2.81,16.36-6.83,5.96-12.98,12.68-18.32,20.02-4.9,6.72-9.11,13.96-12.53,21.63-5.09,11.35-8.47,23.63-9.8,36.51-.43,4.11-.65,8.28-.65,12.5s.22,8.39.65,12.5c1.33,12.88,4.71,25.16,9.8,36.51,3.42,7.67,7.63,14.91,12.53,21.63,7.44,10.24,16.47,19.27,26.72,26.71,6.72,4.91,13.96,9.12,21.63,12.54,11.35,5.09,23.63,8.46,36.5,9.79,4.11.43,8.28.65,12.5.65s8.39-.22,12.5-.65c12.87-1.33,25.15-4.7,36.5-9.79,7.67-3.42,14.91-7.63,21.63-12.54,10.25-7.44,19.28-16.47,26.72-26.71,4.9-6.72,9.11-13.96,12.53-21.63,5.09-11.35,8.47-23.63,9.8-36.51.43-4.11.65-8.28.65-12.5s-.22-8.39-.65-12.5c-1.33-12.88-4.71-25.16-9.8-36.51-3.42-7.67-7.63-14.91-12.53-21.63-5.33-7.34-11.49-14.06-18.32-20.02-4.71-4.11-5.94-10.94-2.81-16.36l6.97-12.07c13.96,9.77,26.13,21.94,35.91,35.9,4.77,6.81,8.97,14.04,12.53,21.63,7.11,15.12,11.68,31.66,13.16,49.06.36,4.12.54,8.29.54,12.5h0Z"/>
<path class="cls-5" d="m290.23,155.57c.52,41.68-26.82,81.58-65.8,96.29v-23.19c0-2.08-1.69-3.77-3.77-3.77h-17.45c-2.08,0-3.77,1.69-3.77,3.77v29.47c-8.3,1.01-16.75,1.02-25.05,0v-124.47c0-2.08-1.69-3.77-3.77-3.77h-17.45c-2.08,0-3.77,1.69-3.77,3.77v118.2c-87.29-34.19-87.26-158.42,0-192.6v43.94c0,2.08,1.69,3.77,3.77,3.77h17.45c2.08,0,3.77-1.69,3.77-3.77v-50.2c8.18-1.01,16.87-1.01,25.05,0v145.2c0,2.08,1.69,3.77,3.77,3.77h17.45c2.08,0,3.77-1.69,3.77-3.77V59.27c39.02,14.78,66.3,54.58,65.8,96.3Z"/>
<path class="cls-4" d="m224.43,255.28c.48-.19.96-.36,1.43-.56-.47.2-.95.39-1.43.57h0Z"/>
<path class="cls-4" d="m256.98,234.93c1.32-1.22,2.61-2.47,3.86-3.74-1.26,1.28-2.54,2.53-3.86,3.74Z"/>
</g>
</g>
</g>
<g>
<path class="cls-3" d="m1813.51,111.36c-10.83-6.33-22.65-9.5-35.45-9.5h-40.87c-1.23,0-2.25.44-3.07,1.33-.82.89-1.23,1.94-1.23,3.17v131.18c0,1.63.58,3.03,1.74,4.19,1.16,1.16,2.55,1.74,4.19,1.74h39.23c12.8,0,24.62-3.17,35.45-9.5,10.83-6.33,19.41-14.92,25.75-25.75,6.33-10.83,9.5-22.65,9.5-35.45s-3.17-24.65-9.5-35.55c-6.33-10.9-14.92-19.51-25.75-25.85Zm3.07,83.67c-4.02,6.81-9.43,12.19-16.24,16.14-6.81,3.95-14.24,5.93-22.27,5.93h-18.8v-86.43c0-.68.24-1.26.71-1.74.48-.48,1.05-.71,1.74-.71h16.35c8.04,0,15.46,2.01,22.27,6.03,6.81,4.02,12.22,9.43,16.24,16.24,4.02,6.81,6.03,14.24,6.03,22.27s-2.01,15.46-6.03,22.27Z"/>
<path class="cls-3" d="m1868.41,126.38h94.81c1.09,0,2.01-.41,2.76-1.23.75-.82,1.12-1.7,1.12-2.66v-17.57c0-.95-.38-1.8-1.12-2.55-.75-.75-1.67-1.12-2.76-1.12h-94.81c-.96,0-1.81.34-2.55,1.02-.75.68-1.12,1.57-1.12,2.66v17.57c0,1.09.37,2.01,1.12,2.76.75.75,1.6,1.12,2.55,1.12Z"/>
<path class="cls-3" d="m1963.63,219.35h-70.29c-.95,0-1.84-.41-2.66-1.23s-1.23-1.7-1.23-2.66v-22.27c0-2.86,1.56-4.29,4.7-4.29h52.11c.95,0,1.77-.31,2.45-.92.68-.61,1.02-1.39,1.02-2.35v-17.57c0-.95-.34-1.74-1.02-2.35-.68-.61-1.5-.92-2.45-.92h-51.49c-3.54,0-5.31-1.29-5.31-3.88v-14.71c0-1.09-.34-1.97-1.02-2.66-.68-.68-1.57-1.02-2.66-1.02h-17.37c-.96,0-1.81.34-2.55,1.02-.75.68-1.12,1.57-1.12,2.66v94.4c0,.95.37,1.81,1.12,2.55.75.75,1.6,1.12,2.55,1.12h95.22c1.09,0,1.97-.37,2.66-1.12.68-.75,1.02-1.6,1.02-2.55v-17.57c0-1.09-.34-1.97-1.02-2.66-.68-.68-1.57-1.02-2.66-1.02Z"/>
<path class="cls-3" d="m2115.9,126.38h94.81c1.09,0,2.01-.41,2.76-1.23.75-.82,1.12-1.7,1.12-2.66v-17.57c0-.95-.38-1.8-1.12-2.55-.75-.75-1.67-1.12-2.76-1.12h-94.81c-.96,0-1.81.34-2.55,1.02-.75.68-1.12,1.57-1.12,2.66v17.57c0,1.09.37,2.01,1.12,2.76.75.75,1.6,1.12,2.55,1.12Z"/>
<path class="cls-3" d="m2211.12,219.35h-70.29c-.96,0-1.84-.41-2.66-1.23s-1.23-1.7-1.23-2.66v-22.27c0-2.86,1.56-4.29,4.7-4.29h52.1c.95,0,1.77-.31,2.45-.92.68-.61,1.02-1.39,1.02-2.35v-17.57c0-.95-.34-1.74-1.02-2.35-.68-.61-1.5-.92-2.45-.92h-51.49c-3.54,0-5.31-1.29-5.31-3.88v-14.71c0-1.09-.34-1.97-1.02-2.66-.68-.68-1.57-1.02-2.66-1.02h-17.37c-.96,0-1.81.34-2.55,1.02-.75.68-1.12,1.57-1.12,2.66v94.4c0,.95.37,1.81,1.12,2.55.75.75,1.6,1.12,2.55,1.12h95.22c1.09,0,1.97-.37,2.66-1.12.68-.75,1.02-1.6,1.02-2.55v-17.57c0-1.09-.34-1.97-1.02-2.66-.68-.68-1.57-1.02-2.66-1.02Z"/>
<path class="cls-3" d="m2323.42,218.33h-49.45c-1.09,0-2.04-.44-2.86-1.33-.82-.88-1.23-1.87-1.23-2.96v-108.7c0-1.09-.41-2.08-1.23-2.96-.82-.88-1.84-1.33-3.06-1.33h-22.07c-1.09,0-2.08.44-2.96,1.33-.89.89-1.33,1.87-1.33,2.96v134.65c0,1.23.44,2.25,1.33,3.07.88.82,1.87,1.23,2.96,1.23h79.89c1.23,0,2.25-.41,3.06-1.23s1.23-1.84,1.23-3.07v-17.57c0-1.09-.41-2.04-1.23-2.86s-1.84-1.23-3.06-1.23Z"/>
<path class="cls-3" d="m2464.73,132.71c-.96,0-1.98.34-3.07,1.02-.82.55-2.79,1.91-5.93,4.09-3.13,2.18-5.25,3.61-6.33,4.29-1.63,1.36-2.45,2.93-2.45,4.7,0,1.36.34,2.73,1.02,4.09,3.4,6.68,5.11,13.89,5.11,21.66,0,5.45-.92,10.8-2.76,16.04-1.84,5.25-4.6,10.05-8.28,14.41-4.36,5.45-9.67,9.6-15.94,12.46-6.27,2.86-12.81,4.29-19.62,4.29-11.17,0-21.25-3.68-30.24-11.03-5.31-4.5-9.4-9.91-12.26-16.24-2.86-6.33-4.29-12.91-4.29-19.72,0-11.31,3.68-21.45,11.03-30.45,3.81-4.9,8.51-8.82,14.1-11.75,5.58-2.93,11.44-4.66,17.57-5.21,2.86-.27,4.29-1.7,4.29-4.29v-17.78c-.14-1.23-.58-2.25-1.33-3.06-.75-.82-1.67-1.23-2.76-1.23h-.41c-9.95.55-19.41,3.03-28.4,7.46-8.99,4.43-16.83,10.73-23.5,18.9-5.59,6.68-9.78,14.07-12.57,22.17-2.79,8.11-4.19,16.38-4.19,24.83,0,10.76,2.25,21.15,6.74,31.16,4.5,10.01,10.96,18.63,19.41,25.85,6.26,5.59,13.42,9.84,21.46,12.77,8.04,2.93,16.35,4.39,24.93,4.39,10.9,0,21.35-2.28,31.37-6.85,10.01-4.56,18.35-11.14,25.03-19.72,5.58-6.67,9.81-14.06,12.67-22.17,2.86-8.1,4.29-16.45,4.29-25.03,0-13.21-3.27-25.54-9.81-36.98-1.09-2.04-2.73-3.06-4.9-3.06Z"/>
<path class="cls-3" d="m2572.09,107.17c-6.95-4.09-14.51-6.13-22.68-6.13h-45.16c-.95,0-1.81.38-2.55,1.12-.75.75-1.12,1.67-1.12,2.76v18.8c0,1.09.37,2.01,1.12,2.76.75.75,1.6,1.12,2.55,1.12h45.16c5.18,0,9.64,1.84,13.38,5.52,3.75,3.68,5.62,8.17,5.62,13.49s-1.87,9.6-5.62,13.28c-3.75,3.68-8.21,5.52-13.38,5.52h-45.16c-.95,0-1.81.38-2.55,1.12-.75.75-1.12,1.67-1.12,2.76v71.31c0,.95.37,1.81,1.12,2.55.75.75,1.6,1.12,2.55,1.12h15.73c1.09,0,2.01-.34,2.76-1.02.75-.68,1.12-1.56,1.12-2.66v-45.57c0-1.23.27-2.04.82-2.45.54-.41,1.56-.61,3.06-.61h21.66c8.17,0,15.73-2.04,22.68-6.13,6.95-4.09,12.46-9.6,16.55-16.55,4.09-6.95,6.13-14.51,6.13-22.68s-2.04-15.77-6.13-22.78c-4.09-7.01-9.6-12.57-16.55-16.65Z"/>
<path class="cls-3" d="m2735.24,101.66h-20.64c-.68,0-1.23.24-1.63.72-.41.48-.82,1.06-1.23,1.74l-30.85,50.67c-.14.27-.48.75-1.02,1.43-.55.68-1.3,1.02-2.25,1.02h-.2c-.96,0-1.74-.27-2.35-.82-.61-.54-.99-.95-1.12-1.23l-31.06-51.29c-.27-.54-.71-1.05-1.33-1.53-.61-.48-1.12-.72-1.53-.72h-20.43c-.96,0-1.81.38-2.55,1.12s-1.12,1.67-1.12,2.76v135.35c0,1.76,1.43,3.19,3.19,3.19h20.8c1.76,0,3.19-1.43,3.19-3.19v-81.61c0-1.23.27-1.84.82-1.84.68,0,1.29.61,1.84,1.84l22.27,31.26c1.09,1.36,2.08,2.25,2.96,2.66.88.41,2.55.61,5.01.61h3.47c2.04,0,3.51-.27,4.39-.82.88-.54,2.01-1.7,3.37-3.47,2.31-3.13,5.96-8.17,10.93-15.12,4.97-6.95,8.55-12.06,10.73-15.32.82-.95,1.43-1.43,1.84-1.43.54,0,.82.61.82,1.84v80.92c0,1.09.34,1.98,1.02,2.66.68.68,1.5,1.02,2.45,1.02h20.23c.95,0,1.77-.34,2.45-1.02s1.02-1.56,1.02-2.66V105.54c0-1.09-.34-2.01-1.02-2.76-.68-.75-1.5-1.12-2.45-1.12Z"/>
<path class="cls-3" d="m2771.32,126.38h94.81c1.09,0,2.01-.41,2.76-1.23.75-.82,1.12-1.7,1.12-2.66v-17.57c0-.95-.38-1.8-1.12-2.55-.75-.75-1.67-1.12-2.76-1.12h-94.81c-.96,0-1.81.34-2.55,1.02-.75.68-1.12,1.57-1.12,2.66v17.57c0,1.09.37,2.01,1.12,2.76.75.75,1.6,1.12,2.55,1.12Z"/>
<path class="cls-3" d="m2866.54,219.35h-70.29c-.96,0-1.84-.41-2.66-1.23s-1.23-1.7-1.23-2.66v-22.27c0-2.86,1.56-4.29,4.7-4.29h52.1c.95,0,1.77-.31,2.45-.92.68-.61,1.02-1.39,1.02-2.35v-17.57c0-.95-.34-1.74-1.02-2.35-.68-.61-1.5-.92-2.45-.92h-51.49c-3.54,0-5.31-1.29-5.31-3.88v-14.71c0-1.09-.34-1.97-1.02-2.66-.68-.68-1.57-1.02-2.66-1.02h-17.37c-.96,0-1.81.34-2.55,1.02-.75.68-1.12,1.57-1.12,2.66v94.4c0,.95.37,1.81,1.12,2.55.75.75,1.6,1.12,2.55,1.12h95.22c1.09,0,1.97-.37,2.66-1.12.68-.75,1.02-1.6,1.02-2.55v-17.57c0-1.09-.34-1.97-1.02-2.66-.68-.68-1.57-1.02-2.66-1.02Z"/>
<path class="cls-3" d="m2985.58,101.45h-21.66c-.95,0-1.81.38-2.55,1.12-.75.75-1.12,1.67-1.12,2.76l.2,79.69c0,2.32-.34,3.47-1.02,3.47-.82,0-1.84-1.16-3.07-3.47l-49.45-82.35c-.68-.82-1.5-1.23-2.45-1.23h-20.02c-1.09,0-2.01.38-2.76,1.12-.75.75-1.12,1.67-1.12,2.76v78.06c0,1.09.37,1.98,1.12,2.66.75.68,1.67,1.02,2.76,1.02h19.82c1.09,0,1.97-.37,2.66-1.12.68-.75,1.02-1.6,1.02-2.55l-.41-24.11c0-.27.03-.68.1-1.23.07-.54.24-.82.51-.82s.82.48,1.63,1.43c6.13,9.81,15.22,24.04,27.28,42.71,12.06,18.67,20.74,32.15,26.05,40.46,1.09,1.36,2.18,2.04,3.27,2.04h19.21c1.09,0,2.01-.34,2.76-1.02.75-.68,1.12-1.56,1.12-2.66V105.33c0-1.09-.38-2.01-1.12-2.76-.75-.75-1.67-1.12-2.76-1.12Z"/>
<path class="cls-3" d="m3099.53,102.27c-.55-.54-1.16-.82-1.84-.82h-89.29c-.68,0-1.3.27-1.84.82s-.82,1.23-.82,2.04v19c0,.82.27,1.5.82,2.04.54.55,1.16.82,1.84.82h11.65c1.36,0,2.52.44,3.47,1.33.95.89,1.43,2.01,1.43,3.37v110.34c0,1.77.88,2.66,2.66,2.66h24.32c.68,0,1.29-.24,1.84-.72.54-.48.82-1.12.82-1.94v-110.34c0-1.23.48-2.31,1.43-3.27.95-.95,2.11-1.43,3.47-1.43h38.21c.68,0,1.29-.27,1.84-.82s.82-1.23.82-2.04v-19c0-.82-.27-1.5-.82-2.04Z"/>
<path class="cls-3" d="m2016.21,121.54l-4.86-17.82c-.41-1.52-1.79-2.57-3.36-2.57l-19.28.03c-2.13,0-3.68,2.03-3.12,4.09l4.98,18.28c.45,1.67,1.97,2.83,3.7,2.83h18.24c2.53,0,4.36-2.4,3.7-4.84Z"/>
<path class="cls-3" d="m2078.75,142.52h-18.35c-1.73,0-3.25,1.16-3.7,2.83-.54,2-1.1,4.05-1.67,6.16-6,22.07-10.56,38.96-13.69,50.68-.55,2.31-1.03,3.47-1.43,3.47s-.95-1.08-1.63-3.23l-4.92-18.07-10.64-39.01c-.45-1.67-1.97-2.83-3.7-2.83h-18.25c-2.53,0-4.36,2.4-3.7,4.84l10.05,36.9h0s.09.35.09.35l15.64,57.4c.28.87.73,1.46,1.32,1.76.68.34,1.7.51,3.07.51h24.93c2.58,0,4.08-.89,4.49-2.66l24.66-90.11,1.13-4.14c.67-2.44-1.17-4.85-3.7-4.85Z"/>
<path class="cls-3" d="m2091.18,101.04l-19.29.03c-.91.06-1.67.26-2.26.59-.75.4-1.26,1.15-1.54,2.24-1.27,4.53-2.9,10.4-4.89,17.62-.67,2.44,1.16,4.86,3.69,4.86h18.37c1.73,0,3.24-1.16,3.7-2.82l4.88-17.82c.27-1.22.13-2.31-.41-3.27-.55-.95-1.3-1.43-2.25-1.43Z"/>
<rect class="cls-3" x="1660.44" y="189.43" width="24.93" height="24.93" rx="2.87" ry="2.87"/>
<rect class="cls-3" x="1660.44" y="130.96" width="24.93" height="24.93" rx="2.87" ry="2.87"/>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 268 KiB

1
static/svelte.svg Normal file
View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="26.6" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 308"><path fill="#FF3E00" d="M239.682 40.707C211.113-.182 154.69-12.301 113.895 13.69L42.247 59.356a82.198 82.198 0 0 0-37.135 55.056a86.566 86.566 0 0 0 8.536 55.576a82.425 82.425 0 0 0-12.296 30.719a87.596 87.596 0 0 0 14.964 66.244c28.574 40.893 84.997 53.007 125.787 27.016l71.648-45.664a82.182 82.182 0 0 0 37.135-55.057a86.601 86.601 0 0 0-8.53-55.577a82.409 82.409 0 0 0 12.29-30.718a87.573 87.573 0 0 0-14.963-66.244"></path><path fill="#FFF" d="M106.889 270.841c-23.102 6.007-47.497-3.036-61.103-22.648a52.685 52.685 0 0 1-9.003-39.85a49.978 49.978 0 0 1 1.713-6.693l1.35-4.115l3.671 2.697a92.447 92.447 0 0 0 28.036 14.007l2.663.808l-.245 2.659a16.067 16.067 0 0 0 2.89 10.656a17.143 17.143 0 0 0 18.397 6.828a15.786 15.786 0 0 0 4.403-1.935l71.67-45.672a14.922 14.922 0 0 0 6.734-9.977a15.923 15.923 0 0 0-2.713-12.011a17.156 17.156 0 0 0-18.404-6.832a15.78 15.78 0 0 0-4.396 1.933l-27.35 17.434a52.298 52.298 0 0 1-14.553 6.391c-23.101 6.007-47.497-3.036-61.101-22.649a52.681 52.681 0 0 1-9.004-39.849a49.428 49.428 0 0 1 22.34-33.114l71.664-45.677a52.218 52.218 0 0 1 14.563-6.398c23.101-6.007 47.497 3.036 61.101 22.648a52.685 52.685 0 0 1 9.004 39.85a50.559 50.559 0 0 1-1.713 6.692l-1.35 4.116l-3.67-2.693a92.373 92.373 0 0 0-28.037-14.013l-2.664-.809l.246-2.658a16.099 16.099 0 0 0-2.89-10.656a17.143 17.143 0 0 0-18.398-6.828a15.786 15.786 0 0 0-4.402 1.935l-71.67 45.674a14.898 14.898 0 0 0-6.73 9.975a15.9 15.9 0 0 0 2.709 12.012a17.156 17.156 0 0 0 18.404 6.832a15.841 15.841 0 0 0 4.402-1.935l27.345-17.427a52.147 52.147 0 0 1 14.552-6.397c23.101-6.006 47.497 3.037 61.102 22.65a52.681 52.681 0 0 1 9.003 39.848a49.453 49.453 0 0 1-22.34 33.12l-71.664 45.673a52.218 52.218 0 0 1-14.563 6.398"></path></svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

BIN
static/tafavicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

BIN
static/talogo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

6
static/tauri.svg Normal file
View file

@ -0,0 +1,6 @@
<svg width="206" height="231" viewBox="0 0 206 231" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M143.143 84C143.143 96.1503 133.293 106 121.143 106C108.992 106 99.1426 96.1503 99.1426 84C99.1426 71.8497 108.992 62 121.143 62C133.293 62 143.143 71.8497 143.143 84Z" fill="#FFC131"/>
<ellipse cx="84.1426" cy="147" rx="22" ry="22" transform="rotate(180 84.1426 147)" fill="#24C8DB"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M166.738 154.548C157.86 160.286 148.023 164.269 137.757 166.341C139.858 160.282 141 153.774 141 147C141 144.543 140.85 142.121 140.558 139.743C144.975 138.204 149.215 136.139 153.183 133.575C162.73 127.404 170.292 118.608 174.961 108.244C179.63 97.8797 181.207 86.3876 179.502 75.1487C177.798 63.9098 172.884 53.4021 165.352 44.8883C157.82 36.3744 147.99 30.2165 137.042 27.1546C126.095 24.0926 114.496 24.2568 103.64 27.6274C92.7839 30.998 83.1319 37.4317 75.8437 46.1553C74.9102 47.2727 74.0206 48.4216 73.176 49.5993C61.9292 50.8488 51.0363 54.0318 40.9629 58.9556C44.2417 48.4586 49.5653 38.6591 56.679 30.1442C67.0505 17.7298 80.7861 8.57426 96.2354 3.77762C111.685 -1.01901 128.19 -1.25267 143.769 3.10474C159.348 7.46215 173.337 16.2252 184.056 28.3411C194.775 40.457 201.767 55.4101 204.193 71.404C206.619 87.3978 204.374 103.752 197.73 118.501C191.086 133.25 180.324 145.767 166.738 154.548ZM41.9631 74.275L62.5557 76.8042C63.0459 72.813 63.9401 68.9018 65.2138 65.1274C57.0465 67.0016 49.2088 70.087 41.9631 74.275Z" fill="#FFC131"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M38.4045 76.4519C47.3493 70.6709 57.2677 66.6712 67.6171 64.6132C65.2774 70.9669 64 77.8343 64 85.0001C64 87.1434 64.1143 89.26 64.3371 91.3442C60.0093 92.8732 55.8533 94.9092 51.9599 97.4256C42.4128 103.596 34.8505 112.392 30.1816 122.756C25.5126 133.12 23.9357 144.612 25.6403 155.851C27.3449 167.09 32.2584 177.598 39.7906 186.112C47.3227 194.626 57.153 200.784 68.1003 203.846C79.0476 206.907 90.6462 206.743 101.502 203.373C112.359 200.002 122.011 193.568 129.299 184.845C130.237 183.722 131.131 182.567 131.979 181.383C143.235 180.114 154.132 176.91 164.205 171.962C160.929 182.49 155.596 192.319 148.464 200.856C138.092 213.27 124.357 222.426 108.907 227.222C93.458 232.019 76.9524 232.253 61.3736 227.895C45.7948 223.538 31.8055 214.775 21.0867 202.659C10.3679 190.543 3.37557 175.59 0.949823 159.596C-1.47592 143.602 0.768139 127.248 7.41237 112.499C14.0566 97.7497 24.8183 85.2327 38.4045 76.4519ZM163.062 156.711L163.062 156.711C162.954 156.773 162.846 156.835 162.738 156.897C162.846 156.835 162.954 156.773 163.062 156.711Z" fill="#24C8DB"/>
</svg>

After

Width:  |  Height:  |  Size: 2.5 KiB

BIN
static/test.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 98 KiB

1
static/vite.svg Normal file
View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

15
svelte.config.js Normal file
View file

@ -0,0 +1,15 @@
// Tauri doesn't have a Node.js server to do proper SSR
// so we will use adapter-static to prerender the app (SSG)
// See: https://v2.tauri.app/start/frontend/sveltekit/ for more info
import adapter from "@sveltejs/adapter-node";
import { vitePreprocess } from "@sveltejs/vite-plugin-svelte";
/** @type {import('@sveltejs/kit').Config} */
const config = {
preprocess: vitePreprocess(),
kit: {
adapter: adapter()
}
};
export default config;

9
tailwind.config.js Normal file
View file

@ -0,0 +1,9 @@
/** @type {import('tailwindcss').Config} */
export default {
content: ['./src/**/*.{html,js,ts,svelte}'],
theme: {
extend: {},
},
plugins: [],
}

19
tsconfig.json Normal file
View 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://kit.svelte.dev/docs/configuration#alias
// except $lib which is handled by https://kit.svelte.dev/docs/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
}

47
vite.config.js Normal file
View file

@ -0,0 +1,47 @@
// @ts-nocheck
import { defineConfig } from "vite";
import { sveltekit } from "@sveltejs/kit/vite";
import markdown from 'vite-plugin-markdown';
// @ts-expect-error process is a nodejs global
const host = process.env.TAURI_DEV_HOST;
// https://vitejs.dev/config/
export default defineConfig(async () => ({
plugins: [
sveltekit(),
],
// Vite options tailored for Tauri development and only applied in `tauri dev` or `tauri build`
//
// 1. prevent vite from obscuring rust errors
clearScreen: false,
// 2. tauri expects a fixed port, fail if that port is not available
server: {
port: 1420,
strictPort: true,
host: host || false,
hmr: host
? {
protocol: "ws",
host,
port: 1421,
}
: undefined,
watch: {
// 3. tell vite to ignore watching `src-tauri`
ignored: ["**/src-tauri/**"],
},
fs: {
allow: ["./", "../", "../../"]
},
optimizeDeps: { exclude: ["fsevents"] },
},
build: {
outDir: 'dist' // Ensure this points to the correct directory
},
preview: {
port: 4173,
host: true, // This is important for Docker
},
}));