Inital Commit for Moonie :)
7
.dockerignore
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
node_modules
|
||||||
|
npm-debug.log
|
||||||
|
dist
|
||||||
|
.vscode
|
||||||
|
.git
|
||||||
|
.gitignore
|
||||||
|
README.md
|
||||||
10
.gitignore
vendored
Normal 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
|
|
@ -0,0 +1,7 @@
|
||||||
|
{
|
||||||
|
"recommendations": [
|
||||||
|
"svelte.svelte-vscode",
|
||||||
|
"tauri-apps.tauri-vscode",
|
||||||
|
"rust-lang.rust-analyzer"
|
||||||
|
]
|
||||||
|
}
|
||||||
3
.vscode/settings.json
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
{
|
||||||
|
"svelte.enable-ts-plugin": true
|
||||||
|
}
|
||||||
23
Dockerfile
Normal 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
|
|
@ -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
44
package.json
Normal 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
6
postcss.config.js
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
export default {
|
||||||
|
plugins: {
|
||||||
|
tailwindcss: {},
|
||||||
|
autoprefixer: {},
|
||||||
|
},
|
||||||
|
}
|
||||||
8
serve-prod.js
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
import { preview } from 'vite'
|
||||||
|
|
||||||
|
const server = await preview({
|
||||||
|
preview: {
|
||||||
|
port: 1470,
|
||||||
|
host: true
|
||||||
|
}
|
||||||
|
})
|
||||||
8
serve-staging.js
Normal 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
|
|
@ -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
25
src-tauri/Cargo.toml
Normal 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
|
|
@ -0,0 +1,3 @@
|
||||||
|
fn main() {
|
||||||
|
tauri_build::build()
|
||||||
|
}
|
||||||
10
src-tauri/capabilities/default.json
Normal 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
|
After Width: | Height: | Size: 3.4 KiB |
BIN
src-tauri/icons/128x128@2x.png
Normal file
|
After Width: | Height: | Size: 6.8 KiB |
BIN
src-tauri/icons/32x32.png
Normal file
|
After Width: | Height: | Size: 974 B |
BIN
src-tauri/icons/Square107x107Logo.png
Normal file
|
After Width: | Height: | Size: 2.8 KiB |
BIN
src-tauri/icons/Square142x142Logo.png
Normal file
|
After Width: | Height: | Size: 3.8 KiB |
BIN
src-tauri/icons/Square150x150Logo.png
Normal file
|
After Width: | Height: | Size: 3.9 KiB |
BIN
src-tauri/icons/Square284x284Logo.png
Normal file
|
After Width: | Height: | Size: 7.6 KiB |
BIN
src-tauri/icons/Square30x30Logo.png
Normal file
|
After Width: | Height: | Size: 903 B |
BIN
src-tauri/icons/Square310x310Logo.png
Normal file
|
After Width: | Height: | Size: 8.4 KiB |
BIN
src-tauri/icons/Square44x44Logo.png
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
BIN
src-tauri/icons/Square71x71Logo.png
Normal file
|
After Width: | Height: | Size: 2 KiB |
BIN
src-tauri/icons/Square89x89Logo.png
Normal file
|
After Width: | Height: | Size: 2.4 KiB |
BIN
src-tauri/icons/StoreLogo.png
Normal file
|
After Width: | Height: | Size: 1.5 KiB |
BIN
src-tauri/icons/icon.icns
Normal file
BIN
src-tauri/icons/icon.ico
Normal file
|
After Width: | Height: | Size: 85 KiB |
BIN
src-tauri/icons/icon.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
14
src-tauri/src/lib.rs
Normal 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
|
|
@ -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
|
|
@ -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
|
|
@ -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>
|
||||||
132
src/lib/components/menus/SideMenuTournaments.svelte
Normal 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>
|
||||||
89
src/lib/components/notifications/InfoPopup.svelte
Normal 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>
|
||||||
332
src/lib/components/notifications/Popup.svelte
Normal 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>
|
||||||
528
src/lib/components/popups/AddNewAuthorisedUser.svelte
Normal 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>
|
||||||
345
src/lib/components/popups/AddNewMapPool.svelte
Normal 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>
|
||||||
1149
src/lib/components/popups/EditMap.svelte
Normal file
6
src/lib/config.json
Normal 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"
|
||||||
|
}
|
||||||
14
src/lib/services/colours.ts
Normal 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
|
|
@ -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'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
156
src/lib/services/taImages.ts
Normal 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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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)
|
||||||
171
src/lib/taDocs/tournamentModel.md
Normal 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
|
|
@ -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">
|
||||||
|
© {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
|
|
@ -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
|
|
@ -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>
|
||||||
40
src/routes/api/docs/[slug]/+server.ts
Normal 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');
|
||||||
|
}
|
||||||
|
}
|
||||||
924
src/routes/authTokens/+page.svelte
Normal 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>
|
||||||
428
src/routes/discordAuth/+page.svelte
Normal 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>
|
||||||
797
src/routes/documentation/+page.svelte
Normal 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>
|
||||||
1124
src/routes/tournaments/+page.svelte
Normal file
661
src/routes/tournaments/[tournamentguid]/+page.svelte
Normal 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>
|
||||||
6
src/routes/tournaments/[tournamentguid]/+page.ts
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
export function load({ params }: any){
|
||||||
|
return {
|
||||||
|
tournamentGuid: params.tournamentguid,
|
||||||
|
params: params
|
||||||
|
}
|
||||||
|
}
|
||||||
707
src/routes/tournaments/[tournamentguid]/mappools/+page.svelte
Normal 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>
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
export function load({ params }: any){
|
||||||
|
return {
|
||||||
|
tournamentGuid: params.tournamentguid,
|
||||||
|
params: params
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
export function load({ params }: any){
|
||||||
|
return {
|
||||||
|
tournamentGuid: params.tournamentguid,
|
||||||
|
poolGuid: params.poolGuid,
|
||||||
|
params: params
|
||||||
|
}
|
||||||
|
}
|
||||||
1130
src/routes/tournaments/[tournamentguid]/settings/+page.svelte
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
export function load({ params }: any){
|
||||||
|
return {
|
||||||
|
tournamentGuid: params.tournamentguid,
|
||||||
|
params: params
|
||||||
|
}
|
||||||
|
}
|
||||||
852
src/routes/tournaments/[tournamentguid]/teams/+page.svelte
Normal 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>
|
||||||
6
src/routes/tournaments/[tournamentguid]/teams/+page.ts
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
export function load({ params }: any){
|
||||||
|
return {
|
||||||
|
tournamentGuid: params.tournamentguid,
|
||||||
|
params: params
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
static/assets/LogoTextC_DevB.png
Normal file
|
After Width: | Height: | Size: 46 KiB |
86
static/assets/LogoTextC_DevB.svg
Normal 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 |
BIN
static/assets/LogoTextC_DevB4x.png
Normal file
|
After Width: | Height: | Size: 275 KiB |
BIN
static/assets/LogoTextC_DevW.png
Normal file
|
After Width: | Height: | Size: 44 KiB |
90
static/assets/LogoTextC_DevW.svg
Normal 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 |
BIN
static/assets/LogoTextC_DevW4x.png
Normal file
|
After Width: | Height: | Size: 268 KiB |
1
static/svelte.svg
Normal 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
|
After Width: | Height: | Size: 26 KiB |
BIN
static/talogo.png
Normal file
|
After Width: | Height: | Size: 25 KiB |
6
static/tauri.svg
Normal 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
|
After Width: | Height: | Size: 98 KiB |
1
static/vite.svg
Normal 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
|
|
@ -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
|
|
@ -0,0 +1,9 @@
|
||||||
|
/** @type {import('tailwindcss').Config} */
|
||||||
|
export default {
|
||||||
|
content: ['./src/**/*.{html,js,ts,svelte}'],
|
||||||
|
theme: {
|
||||||
|
extend: {},
|
||||||
|
},
|
||||||
|
plugins: [],
|
||||||
|
}
|
||||||
|
|
||||||
19
tsconfig.json
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
{
|
||||||
|
"extends": "./.svelte-kit/tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"allowJs": true,
|
||||||
|
"checkJs": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"sourceMap": true,
|
||||||
|
"strict": true,
|
||||||
|
"moduleResolution": "bundler"
|
||||||
|
}
|
||||||
|
// Path aliases are handled by https://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
|
|
@ -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
|
||||||
|
},
|
||||||
|
}));
|
||||||