roles and docs update

This commit is contained in:
Luna 2025-09-24 13:41:04 +02:00
parent 1922ee58ec
commit 1cff66b0d9
12 changed files with 1258 additions and 299 deletions

View file

@ -1,3 +1,4 @@
{ {
"svelte.enable-ts-plugin": true "svelte.enable-ts-plugin": true,
"typescript.experimental.useTsgo": true
} }

View file

@ -1,12 +1,13 @@
<script lang="ts"> <script lang="ts">
import { createEventDispatcher } from "svelte"; import { createEventDispatcher } from "svelte";
import { TAClient } from "moons-ta-client"; import { TAClient, type Role } from "moons-ta-client";
const dispatch = createEventDispatcher(); const dispatch = createEventDispatcher();
export let isOpen: boolean = false; export let isOpen: boolean = false;
export let taClient: TAClient; export let taClient: TAClient;
export let tournamentGuid: string; export let tournamentGuid: string;
export let tournamentRoles: Role[] = [];
// User states // User states
let discordUserId: string = ""; let discordUserId: string = "";
@ -21,9 +22,8 @@
discordAvatarUrl: string; discordAvatarUrl: string;
} | null = null; } | null = null;
// Permission settings // Role selection
let hasAdminPermission: boolean = false; let selectedRoles: Set<string> = new Set();
let hasViewPermission: boolean = true;
// This will be implemented by the parent component // This will be implemented by the parent component
async function fetchDiscordUser() { async function fetchDiscordUser() {
@ -74,8 +74,7 @@
discordUser = null; discordUser = null;
userFound = false; userFound = false;
error = null; error = null;
hasAdminPermission = false; selectedRoles = new Set();
hasViewPermission = true;
} }
function closePopup() { function closePopup() {
@ -83,32 +82,22 @@
dispatch("close"); dispatch("close");
} }
function handleRoleToggle(roleId: string) {
if (selectedRoles.has(roleId)) {
selectedRoles.delete(roleId);
} else {
selectedRoles.add(roleId);
}
selectedRoles = selectedRoles; // Trigger reactivity
}
function addUser() { function addUser() {
if (discordUser) { 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", { dispatch("addUser", {
discordId: discordUser.discordId, discordId: discordUser.discordId,
discordUsername: discordUser.discordUsername, discordUsername: discordUser.discordUsername,
discordAvatarUrl: discordUser.discordAvatarUrl, discordAvatarUrl: discordUser.discordAvatarUrl,
permission roleIds: Array.from(selectedRoles)
}); });
closePopup(); closePopup();
@ -176,30 +165,27 @@
</div> </div>
</div> </div>
<div class="permissions-section"> <div class="roles-section">
<h4>Permissions</h4> <h4>Tournament Roles</h4>
<div class="permission-option"> {#if tournamentRoles.length === 0}
<div class="permission-label"> <p class="no-roles-message">No tournament roles available</p>
<label for="view-permission">View Permission</label> {:else}
<p class="permission-description">Can view tournament data (generally players)</p> {#each tournamentRoles as role (role.guid)}
</div> <div class="role-option">
<label class="toggle"> <div class="role-label">
<input type="checkbox" id="view-permission" bind:checked={hasViewPermission}> <label for="role-{role.guid}">{role.name}</label>
<span class="slider"></span> </div>
</label> <input
</div> type="checkbox"
id="role-{role.guid}"
<div class="permission-option"> checked={selectedRoles.has(role.roleId)}
<div class="permission-label"> on:change={() => handleRoleToggle(role.roleId)}
<label for="admin-permission">Admin Permission</label> class="role-checkbox"
<p class="permission-description">Can manage tournament settings (generally staff))</p> />
</div> </div>
<label class="toggle"> {/each}
<input type="checkbox" id="admin-permission" bind:checked={hasAdminPermission}> {/if}
<span class="slider"></span>
</label>
</div>
</div> </div>
</div> </div>
{/if} {/if}
@ -210,7 +196,7 @@
<button <button
class="add-button" class="add-button"
on:click={addUser} on:click={addUser}
disabled={!userFound || (!hasViewPermission && !hasAdminPermission)} disabled={!userFound || selectedRoles.size === 0}
> >
Add User Add User
</button> </button>
@ -381,19 +367,29 @@
color: var(--text-secondary); color: var(--text-secondary);
} }
.permissions-section { .roles-section {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 1rem; gap: 1rem;
} }
.permissions-section h4 { .roles-section h4 {
margin: 0 0 0.5rem 0; margin: 0 0 0.5rem 0;
font-size: 1rem; font-size: 1rem;
font-weight: 500; font-weight: 500;
} }
.permission-option { .no-roles-message {
margin: 0;
font-size: 0.875rem;
color: var(--text-secondary);
text-align: center;
padding: 1rem;
background-color: var(--bg-tertiary);
border-radius: 0.5rem;
}
.role-option {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
@ -402,69 +398,22 @@
border-radius: 0.5rem; border-radius: 0.5rem;
} }
.permission-label { .role-label {
flex: 1; flex: 1;
} }
.permission-label label { .role-label label {
font-weight: 500; font-weight: 500;
font-size: 0.9375rem; 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; cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: var(--bg-secondary);
transition: .4s;
border-radius: 1.5rem;
} }
.slider:before { .role-checkbox {
position: absolute;
content: "";
height: 1.125rem;
width: 1.125rem; width: 1.125rem;
left: 0.1875rem; height: 1.125rem;
bottom: 0.1875rem; margin-left: 1rem;
background-color: var(--text-secondary); cursor: pointer;
transition: .4s; accent-color: var(--accent-color);
border-radius: 50%;
}
input:checked + .slider {
background-color: var(--accent-color);
}
input:checked + .slider:before {
transform: translateX(1.5rem);
background-color: white;
} }
.popup-actions { .popup-actions {

View file

@ -0,0 +1,557 @@
<script lang="ts">
import { createEventDispatcher } from "svelte";
import { type Role } from 'moons-ta-client'
interface InternalPermission {
displayName: string,
value: string,
description: string
}
const dispatch = createEventDispatcher();
export let isOpen: boolean = false;
export let role: Role | null = null;
export let tournamentRoles: Role[] = []
let availablePermissions: InternalPermission[] = [
{ displayName: "View Tournament In List", value: "tournament:view_tournament_in_list", description: "" },
{ displayName: "Join Tournament", value: "tournament:join", description: "" },
{ displayName: "Add Authorized Users", value: "tournament:settings:add_authorized_users", description: "" },
{ displayName: "Update Authorized User Roles", value: "tournament:settings:update_authorized_user_roles", description: "" },
{ displayName: "Remove Authorized Users", value: "tournament:settings:remove_authorized_users", description: "" },
{ displayName: "Get Authorized Users", value: "tournament:settings:get_authorized_users", description: "" },
{ displayName: "Get Discord Info", value: "tournament:settings:get_discord_info", description: "" },
{ displayName: "Get Qualifier Scores", value: "tournament:qualifier:get_qualifier_scores", description: "" },
{ displayName: "Submit Qualifier Scores", value: "tournament:qualifier:submit_qualifier_scores", description: "" },
{ displayName: "See Hidden Qualifier Scores", value: "tournament:qualifier:see_hidden_qualifier_scores", description: "" },
{ displayName: "Get Remaining Attempts", value: "tournament:qualifier:get_remaining_attempts", description: "" },
{ displayName: "Refund Attempts", value: "tournament:qualifier:refund_attempts", description: "" },
{ displayName: "Return To Menu", value: "tournament:player:return_to_menu", description: "" },
{ displayName: "Play Song", value: "tournament:player:play_song", description: "" },
{ displayName: "Play With Stream Sync", value: "tournament:player:play_with_stream_sync", description: "" },
{ displayName: "Modify Gameplay", value: "tournament:player:modify_gameplay", description: "" },
{ displayName: "Load Song", value: "tournament:player:load_song", description: "" },
{ displayName: "Create Match", value: "tournament:match:create_match", description: "" },
{ displayName: "Add User To Match", value: "tournament:match:add_user_to_match", description: "" },
{ displayName: "Remove User From Match", value: "tournament:match:remove_user_from_match", description: "" },
{ displayName: "Set Match Leader", value: "tournament:match:set_match_leader", description: "" },
{ displayName: "Set Match Map", value: "tournament:match:set_match_map", description: "" },
{ displayName: "Delete Match", value: "tournament:match:delete_match", description: "" },
{ displayName: "Create Qualifier", value: "tournament:qualifier:create", description: "" },
{ displayName: "Set Qualifier Name", value: "tournament:qualifier:set_name", description: "" },
{ displayName: "Set Qualifier Image", value: "tournament:qualifier:set_image", description: "" },
{ displayName: "Set Qualifier Info Channel", value: "tournament:qualifier:set_info_channel", description: "" },
{ displayName: "Set Qualifier Flags", value: "tournament:qualifier:set_flags", description: "" },
{ displayName: "Set Qualifier Leaderboard Sort", value: "tournament:qualifier:set_leaderboard_sort", description: "" },
{ displayName: "Add Qualifier Maps", value: "tournament:qualifier:add_maps", description: "" },
{ displayName: "Update Qualifier Map", value: "tournament:qualifier:update_map", description: "" },
{ displayName: "Remove Qualifier Map", value: "tournament:qualifier:remove_map", description: "" },
{ displayName: "Delete Qualifier", value: "tournament:qualifier:delete", description: "" },
{ displayName: "Set Tournament Name", value: "tournament:settings:set_name", description: "" },
{ displayName: "Set Tournament Image", value: "tournament:settings:set_image", description: "" },
{ displayName: "Set Tournament Enable Teams", value: "tournament:settings:set_enable_teams", description: "" },
{ displayName: "Set Tournament Enable Pools", value: "tournament:settings:set_enable_pools", description: "" },
{ displayName: "Set Tournament Show Tournament Button", value: "tournament:settings:set_show_tournament_button", description: "" },
{ displayName: "Set Tournament Show Qualifier Button", value: "tournament:settings:set_show_qualifier_button", description: "" },
{ displayName: "Set Tournament Allow Unauthorized View", value: "tournament:settings:set_allow_unauthorized_view", description: "" },
{ displayName: "Set Tournament Score Update Frequency", value: "tournament:settings:set_score_update_frequency", description: "" },
{ displayName: "Set Tournament Banned Mods", value: "tournament:settings:set_banned_mods", description: "" },
{ displayName: "Add Tournament Team", value: "tournament:settings:add_team", description: "" },
{ displayName: "Set Tournament Team Name", value: "tournament:settings:set_team_name", description: "" },
{ displayName: "Set Tournament Team Image", value: "tournament:settings:set_team_image", description: "" },
{ displayName: "Remove Tournament Team", value: "tournament:settings:remove_team", description: "" },
{ displayName: "Add Tournament Pool", value: "tournament:settings:add_pool", description: "" },
{ displayName: "Set Tournament Pool Name", value: "tournament:settings:set_pool_name", description: "" },
{ displayName: "Set Tournament Pool Image", value: "tournament:settings:set_pool_image", description: "" },
{ displayName: "Add Tournament Pool Maps", value: "tournament:settings:add_pool_maps", description: "" },
{ displayName: "Update Tournament Pool Maps", value: "tournament:settings:update_pool_maps", description: "" },
{ displayName: "Remove Tournament Pool Maps", value: "tournament:settings:remove_pool_maps", description: "" },
{ displayName: "Remove Tournament Pools", value: "tournament:settings:remove_pools", description: "" },
{ displayName: "Add Tournament Role", value: "tournament:settings:add_role", description: "" },
{ displayName: "Set Tournament Role Name", value: "tournament:settings:set_role_name", description: "" },
{ displayName: "Set Tournament Role Permissions", value: "tournament:settings:set_role_permissions", description: "" },
{ displayName: "Remove Tournament Role", value: "tournament:settings:remove_role", description: "" },
{ displayName: "Delete Tournament", value: "tournament:settings:delete", description: "" },
];
// Form states
let roleName: string = "";
let roleId: string = "";
let selectedPermissions: Set<string> = new Set();
let error: string | null = null;
// Reactive statement to initialize form when role or popup opens
$: if (isOpen) {
initializeForm();
}
function initializeForm() {
if (role) {
// Editing existing role
roleName = role.name;
roleId = role.roleId;
selectedPermissions = new Set(role.permissions);
} else {
// Creating new role
roleName = "";
selectedPermissions = new Set();
}
error = null;
}
function resetForm() {
roleName = "";
roleId = "";
selectedPermissions = new Set();
error = null;
}
function closePopup() {
resetForm();
isOpen = false;
}
function handlePermissionToggle(permissionValue: string) {
if (selectedPermissions.has(permissionValue)) {
selectedPermissions.delete(permissionValue);
} else {
selectedPermissions.add(permissionValue);
}
selectedPermissions = selectedPermissions; // Trigger reactivity
}
function selectAllPermissions() {
selectedPermissions = new Set(availablePermissions.map(p => p.value));
}
function deselectAllPermissions() {
selectedPermissions = new Set();
}
function validateForm(): boolean {
if (roleName.trim() === "") {
error = "Role name is required";
return false;
}
if (selectedPermissions.size === 0) {
error = "At least one permission must be selected";
return false;
}
error = null;
return true;
}
function handleSave() {
if (!validateForm()) {
return;
}
const permissions = Array.from(selectedPermissions);
if (role) {
// Modifying existing role
dispatch("modifyRole", {
roleId: role.roleId,
name: roleName.trim(),
permissions: permissions
});
} else {
// Creating new role
dispatch("createRole", {
name: roleName.trim(),
roleId: roleId,
permissions: permissions
});
}
closePopup();
}
function handleKeyDown(event: KeyboardEvent) {
if (event.key === "Escape") {
closePopup();
}
}
</script>
<svelte:window on:keydown={handleKeyDown} />
{#if isOpen}
<div class="popup-overlay">
<div class="popup-container">
<div class="popup-header">
<h3>{role ? 'Modify Role' : 'Create Role'}</h3>
<button class="close-button" on:click={closePopup}>
<span class="material-icons">close</span>
</button>
</div>
<div class="popup-content">
<div class="role-name-section">
<label for="role-name">Role Name</label>
<input
type="text"
id="role-name"
bind:value={roleName}
placeholder="Enter role name"
class="role-name-input"
/>
</div>
{#if !role}
<div class="role-name-section">
<label for="role-id">Role ID</label>
<input
type="text"
id="role-id"
bind:value={roleId}
placeholder="Enter role id"
class="role-name-input"
/>
</div>
{/if}
{#if error}
<p class="error-message">{error}</p>
{/if}
<div class="permissions-section">
<div class="permissions-header">
<h4>Permissions ({selectedPermissions.size} of {availablePermissions && availablePermissions.length ? availablePermissions.length : 0} selected)</h4>
<div class="permissions-actions">
<button class="select-all-button" on:click={selectAllPermissions}>
Select All
</button>
<button class="deselect-all-button" on:click={deselectAllPermissions}>
Deselect All
</button>
</div>
</div>
<div class="permissions-grid">
{#each availablePermissions as permission (permission.value)}
<div class="permission-option">
<div class="permission-label">
<label for="permission-{permission.value}">
{permission.displayName}
</label>
</div>
<input
type="checkbox"
id="permission-{permission.value}"
checked={selectedPermissions.has(permission.value)}
on:change={() => handlePermissionToggle(permission.value)}
class="permission-checkbox"
/>
</div>
{/each}
</div>
</div>
</div>
<div class="popup-actions">
<button class="cancel-button" on:click={closePopup}>Cancel</button>
<button
class="save-button"
on:click={handleSave}
disabled={roleName.trim() === "" || selectedPermissions.size === 0}
>
{role ? 'Save Changes' : 'Create Role'}
</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;
padding: 1rem;
}
.popup-container {
background-color: var(--bg-primary);
border-radius: 0.75rem;
width: 100%;
max-width: 56rem;
max-height: 90vh;
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);
flex-shrink: 0;
}
.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;
overflow-y: auto;
}
.role-name-section {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.role-name-section label {
font-weight: 500;
font-size: 0.9375rem;
}
.role-name-input {
background-color: var(--bg-tertiary);
border: none;
border-radius: 0.375rem;
padding: 0.625rem 0.75rem;
color: var(--text-primary);
font-size: 0.9375rem;
}
.role-name-input:focus {
outline: 2px solid var(--accent-color);
}
.error-message {
color: #ff5555;
font-size: 0.875rem;
margin: 0;
padding: 0.5rem;
background-color: rgba(255, 85, 85, 0.1);
border-radius: 0.375rem;
border: 1px solid rgba(255, 85, 85, 0.3);
}
.permissions-section {
display: flex;
flex-direction: column;
gap: 1rem;
flex: 1;
}
.permissions-header {
display: flex;
justify-content: space-between;
align-items: center;
gap: 1rem;
flex-wrap: wrap;
}
.permissions-header h4 {
margin: 0;
font-size: 1rem;
font-weight: 500;
}
.permissions-actions {
display: flex;
gap: 0.5rem;
}
.select-all-button,
.deselect-all-button {
background-color: var(--bg-tertiary);
border: none;
border-radius: 0.375rem;
padding: 0.5rem 0.75rem;
font-size: 0.875rem;
font-weight: 500;
color: var(--text-secondary);
cursor: pointer;
transition: all 0.2s;
}
.select-all-button:hover {
background-color: var(--accent-color);
color: white;
}
.deselect-all-button:hover {
background-color: #ff5555;
color: white;
}
.permissions-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 0.75rem;
max-height: 400px;
overflow-y: auto;
padding: 0.5rem;
border-radius: 0.5rem;
background-color: var(--bg-secondary);
}
.permission-option {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.75rem;
background-color: var(--bg-tertiary);
border-radius: 0.5rem;
transition: background-color 0.2s;
}
.permission-option:hover {
background-color: var(--bg-primary);
}
.permission-label {
flex: 1;
margin-right: 0.5rem;
}
.permission-label label {
font-weight: 400;
font-size: 0.875rem;
cursor: pointer;
word-wrap: break-word;
line-height: 1.3;
}
.permission-checkbox {
width: 1.125rem;
height: 1.125rem;
cursor: pointer;
accent-color: var(--accent-color);
flex-shrink: 0;
}
.popup-actions {
display: flex;
justify-content: flex-end;
gap: 1rem;
padding: 1.25rem 1.5rem;
border-top: 1px solid var(--bg-tertiary);
flex-shrink: 0;
}
.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);
}
.save-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;
}
.save-button:hover {
background-color: var(--accent-hover);
}
.save-button:disabled {
background-color: var(--bg-tertiary);
color: var(--text-secondary);
cursor: not-allowed;
}
/* Scrollbar styling */
.permissions-grid::-webkit-scrollbar {
width: 6px;
}
.permissions-grid::-webkit-scrollbar-track {
background: var(--bg-tertiary);
border-radius: 3px;
}
.permissions-grid::-webkit-scrollbar-thumb {
background: var(--text-secondary);
border-radius: 3px;
}
.permissions-grid::-webkit-scrollbar-thumb:hover {
background: var(--text-primary);
}
/* Responsive design */
@media (max-width: 768px) {
.popup-container {
margin: 0.5rem;
max-width: calc(100% - 1rem);
}
.permissions-grid {
grid-template-columns: 1fr;
max-height: 300px;
}
.permissions-header {
flex-direction: column;
align-items: flex-start;
}
.permissions-actions {
width: 100%;
justify-content: flex-end;
}
}
</style>

View file

@ -1,9 +1,9 @@
# Introduction # 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 a RESTful 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. TournamentAssistant is a piece of software 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 a RESTful 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.
## Prerequisites ## Prerequisites
You should probably get familiar with: You should probably get familiar with:
- Serialisation - Serialisation - optional, but recommended
- Typescript - Typescript
- Websocket connections - Websocket connections

View file

@ -0,0 +1,46 @@
# The taClient.addAuthorizedUser() method
This documents the `taClient.addAuthorizedUser()` method.
This method has input parameters. The function could easily be defined as:
```ts
async function addAuthorizedUser(tournamentId: string, discordId: string, roleIds: string[]): Promise<Response> {
// backend logic
}
```
## General usage:
```ts
const response: Response = taClient.addAuthorizedUser(tournamentGuid, userDiscordId, ['roleId1', 'roleId2'])
```
The `response` variable from above will actually be a 'mashup' of two types. Firstly, the standard `Response` type. Secondly the `Response_AddAuthorizedUser`. Find the custom response object below.
- For `Response` please find documentation at [Modals -> Response](/documentation#Modals/4-Response)
- For `Response_AddAuthorizedUser` please find documentation at [Modals -> Response_AddAuthorizedUser](/documentation#Modals/4-Response_AddAuthorizedUser).
```js
{
details: {
addAuthorizedUser: Response_AddAuthorizedUser,
oneofKind: 'addAuthorizedUser'
},
respondingToPacketId: 'packet-guid-uuidv4',
type: Response_ResponseType
}
```
<div class="info-box">
<span class="material-icons">info</span>
<div>
<strong>Note:</strong> The method uses role.roleId, not role.guid.
</div>
</div>
```ts
ts example here
```
<div class="info-box">
<span class="material-icons">info</span>
<div>
<strong>Example:</strong> A direct example is not available, as this function is not used in ShyyTAUI.
</div>
</div>

View file

@ -10,6 +10,7 @@ The table below shows all of the functions of the taClient. From this point onwa
|addServer()|Add a CoreServer. You will probably never use this.|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| |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| |addTournamentPoolMaps()|Add maps to an already existing map pool.|True|
|addTournamentRole()|Add a role to a tournament.|True|
|addTournamentTeam()|Add a team to a tournament. This will create a new team|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| |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| |connect()|Connect to a TournamentAssistant Core Server. This is the main server, not the tournament.|True|
@ -42,6 +43,7 @@ The table below shows all of the functions of the taClient. From this point onwa
|removeQualifierMap()|Remove a qualifier map from an already existing qualifier.|True| |removeQualifierMap()|Remove a qualifier map from an already existing qualifier.|True|
|removeTournamentPool()|Remove a map pool from a tournament.|True| |removeTournamentPool()|Remove a map pool from a tournament.|True|
|removeTournamentPoolMap()|Remove a map from an already existing map pool.|True| |removeTournamentPoolMap()|Remove a map from an already existing map pool.|True|
|removeTournamentRole()|Remove a role from a tournament.|True|
|removeTournamentTeam()|Remove a team from a tournament.|True| |removeTournamentTeam()|Remove a team from a tournament.|True|
|removeUserFromMatch()|Remove a user from a match.|True| |removeUserFromMatch()|Remove a user from a match.|True|
|returnToMenu()|Return the speciied players to the menu.|False| |returnToMenu()|Return the speciied players to the menu.|False|

View file

@ -3,16 +3,40 @@ The `taClient`(client for short) is how most of the actions will be carried out.
The `stateManager` is the way you would fetch tournaments, get matches, or do anything related to getting specific data that does not require a database query. It is best if you are familiar with what methods it has and how you can use them. The `stateManager` is the way you would fetch tournaments, get matches, or do anything related to getting specific data that does not require a database query. It is best if you are familiar with what methods it has and how you can use them.
For the ease of this explanation let me provide an example: ## Standard Response Object
There are a couple of important notes for client responses. Since most client responses are async, they will return a promise. This will be shown as `Promise<Response>` in IntelliSense. In reality, what happens under the hood is that ProtoBuf sends back a packet of the form:
```ts
{
details: {
packetType: data,
oneofKind: 'packetType'
},
respondingToPacketId: 'packet-guid-uuidv4',
type: Response_ResponseType
}
```
For the definition of the `Response_ResponseType` look at the [Modals -> Response_ResponseType](/documentation#Modals/4-Response_ResponseType)
<div class="warning-box">
<span class="material-icons">question_mark</span>
<div>
<strong>Why is this important?</strong> TypeScript <strong>DOES NOT</strong> recognise these `packetType` objects, hence when dealing with responses we often use `as any` to get rid of type errors.
</div>
</div>
Let me provide an example of how the client is used in ShyyTAUI:
```ts ```ts
onMount(async() => { onMount(async() => {
// Check if the user is logged in // Check if the user is logged in
if ($authTokenStore) { if ($authTokenStore) {
// Using the client listen to events that will not be stored in the state, such as scores and song finish events // Using the client listen for events that will not be stored in the state,
// such as scores and song finish events
client.on('realtimeScore', handleRealtimeScoreUpdate); client.on('realtimeScore', handleRealtimeScoreUpdate);
client.on('songFinished', handleSongFinished); client.on('songFinished', handleSongFinished);
} else { } else {
// If the user is not authenticated, they will be thrown to the discord authentication page // If the user is not authenticated,
// they will be thrown to the discord authentication page
window.location.href = "/discordAuth" window.location.href = "/discordAuth"
} }
}); });

View file

@ -13,6 +13,20 @@ Some more normal text...
</div> </div>
</div> </div>
<div class="warning-box">
<span class="material-icons">question_mark</span>
<div>
<strong>Will the stateManager be out-of-date?</strong> The stateManager automatically updates its state when a packet is received or sent, so no, you do not have to worry about out-of-date information being stored.
</div>
</div>
<div class="warning-box">
<span class="material-icons">warning</span>
<div>
<strong>Note:</strong> This will be explained further in the 'State Manager' section, along with further behaviour descriptors. (most info will even be repeated)
</div>
</div>
## Support ## Support
If you have any issues, please contact us through Discord or email If you have any issues, please contact us through Discord or email

View file

@ -7,6 +7,7 @@
let isAuthenticated = false; let isAuthenticated = false;
let profileDropdownOpen = false; let profileDropdownOpen = false;
let mobileMenuOpen = false;
let userProfile: { name: string; avatar?: string } | null = null; let userProfile: { name: string; avatar?: string } | null = null;
onMount(() => { onMount(() => {
@ -25,6 +26,10 @@
profileDropdownOpen = !profileDropdownOpen; profileDropdownOpen = !profileDropdownOpen;
} }
function toggleMobileMenu() {
mobileMenuOpen = !mobileMenuOpen;
}
function logout() { function logout() {
authTokenStore.set(null); authTokenStore.set(null);
discordDataStore.set(null); discordDataStore.set(null);
@ -32,14 +37,27 @@
isAuthenticated = false; isAuthenticated = false;
userProfile = null; userProfile = null;
profileDropdownOpen = false; profileDropdownOpen = false;
mobileMenuOpen = false;
goto('/'); goto('/');
} }
function handleClickOutside(event: MouseEvent) { function handleClickOutside(event: MouseEvent) {
const dropdown = document.querySelector('.profile-container'); const dropdown = document.querySelector('.profile-container');
const mobileMenu = document.querySelector('.mobile-menu');
const hamburger = document.querySelector('.hamburger-button');
if (dropdown && !dropdown.contains(event.target as Node) && profileDropdownOpen) { if (dropdown && !dropdown.contains(event.target as Node) && profileDropdownOpen) {
profileDropdownOpen = false; profileDropdownOpen = false;
} }
if (mobileMenu && !mobileMenu.contains(event.target as Node) &&
hamburger && !hamburger.contains(event.target as Node) && mobileMenuOpen) {
mobileMenuOpen = false;
}
}
function handleNavClick() {
mobileMenuOpen = false;
} }
</script> </script>
@ -52,7 +70,8 @@
<img class="logo-svg" src="/assets/LogoTextC_DevW.svg" alt=""> <img class="logo-svg" src="/assets/LogoTextC_DevW.svg" alt="">
</div> </div>
<nav class="navbar-section nav-links"> <!-- Desktop Navigation -->
<nav class="navbar-section nav-links desktop-nav">
<a href="/" class:active={$page.url.pathname === '/'}> <a href="/" class:active={$page.url.pathname === '/'}>
<span>Home</span> <span>Home</span>
<div class="hover-indicator"></div> <div class="hover-indicator"></div>
@ -67,7 +86,8 @@
</a> </a>
</nav> </nav>
<div class="navbar-section profile-container"> <!-- Desktop Profile -->
<div class="navbar-section profile-container desktop-profile">
<button class="profile-button" on:click={toggleProfileDropdown}> <button class="profile-button" on:click={toggleProfileDropdown}>
{#if isAuthenticated && userProfile?.avatar} {#if isAuthenticated && userProfile?.avatar}
<img src={userProfile.avatar} alt="Profile" class="profile-image" /> <img src={userProfile.avatar} alt="Profile" class="profile-image" />
@ -80,7 +100,7 @@
{#if profileDropdownOpen} {#if profileDropdownOpen}
<div class="profile-dropdown" transition:slide={{duration: 200}}> <div class="profile-dropdown" transition:slide={{duration: 200}}>
{#if isAuthenticated} {#if isAuthenticated}
<a href="/authTokens" class="dropdown-item"> <a href="/authTokens" class="dropdown-item" on:click={handleNavClick}>
<span class="material-icons">vpn_key</span> <span class="material-icons">vpn_key</span>
<span>Manage Auth (MAT)</span> <span>Manage Auth (MAT)</span>
</a> </a>
@ -90,7 +110,7 @@
<span>Logout</span> <span>Logout</span>
</button> </button>
{:else} {:else}
<a href="/discordAuth" class="dropdown-item"> <a href="/discordAuth" class="dropdown-item" on:click={handleNavClick}>
<span class="material-icons">login</span> <span class="material-icons">login</span>
<span>Login</span> <span>Login</span>
</a> </a>
@ -98,7 +118,55 @@
</div> </div>
{/if} {/if}
</div> </div>
<!-- Mobile Hamburger Menu -->
<button class="hamburger-button mobile-only" on:click={toggleMobileMenu}>
<span class="material-icons">{mobileMenuOpen ? 'close' : 'menu'}</span>
</button>
</div> </div>
<!-- Mobile Menu -->
{#if mobileMenuOpen}
<div class="mobile-menu" transition:slide={{duration: 300}}>
<nav class="mobile-nav-links">
<a href="/" class:active={$page.url.pathname === '/'} on:click={handleNavClick}>
<span class="material-icons">home</span>
<span>Home</span>
</a>
<a href="/tournaments" class:active={$page.url.pathname === '/tournaments'} on:click={handleNavClick}>
<span class="material-icons">sports_esports</span>
<span>View Tournaments</span>
</a>
<a href="/authTokens" class:active={$page.url.pathname === '/authTokens'} on:click={handleNavClick}>
<span class="material-icons">vpn_key</span>
<span>My Tokens</span>
</a>
</nav>
<div class="mobile-profile-section">
{#if isAuthenticated}
<div class="mobile-user-info">
{#if userProfile?.avatar}
<img src={userProfile.avatar} alt="Profile" class="mobile-profile-image" />
{:else}
<span class="material-icons">account_circle</span>
{/if}
<span class="mobile-user-name">{userProfile?.name}</span>
</div>
<div class="mobile-profile-divider"></div>
<button class="mobile-logout-button" on:click={logout}>
<span class="material-icons">exit_to_app</span>
<span>Logout</span>
</button>
{:else}
<a href="/discordAuth" class="mobile-login-button" on:click={handleNavClick}>
<span class="material-icons">login</span>
<span>Login</span>
</a>
{/if}
</div>
</div>
{/if}
</header> </header>
<main> <main>
@ -195,7 +263,6 @@
position: sticky; position: sticky;
top: 0; top: 0;
z-index: 100; z-index: 100;
height: var(--navbar-height);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2); box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
} }
@ -203,7 +270,7 @@
display: grid; display: grid;
grid-template-columns: 1fr auto 1fr; grid-template-columns: 1fr auto 1fr;
align-items: center; align-items: center;
height: 100%; height: var(--navbar-height);
padding: 0 1.5rem; padding: 0 1.5rem;
max-width: 1200px; max-width: 1200px;
margin: 0 auto; margin: 0 auto;
@ -217,17 +284,16 @@
.logo-section { .logo-section {
justify-content: flex-start; justify-content: flex-start;
right: 5rem;
padding-right: 10rem;
} }
.logo-svg { .logo-svg {
height: 40px; height: 40px;
width: auto; width: auto;
max-width: none;
} }
/* Navigation links - centered */ /* Desktop Navigation */
.nav-links { .desktop-nav {
justify-content: center; justify-content: center;
gap: 2rem; gap: 2rem;
height: 100%; height: 100%;
@ -277,8 +343,8 @@
background: linear-gradient(90deg, transparent, var(--accent-hover), transparent); background: linear-gradient(90deg, transparent, var(--accent-hover), transparent);
} }
/* Profile section */ /* Desktop Profile */
.profile-container { .desktop-profile {
position: relative; position: relative;
justify-content: flex-end; justify-content: flex-end;
} }
@ -364,6 +430,132 @@
margin: 0.5rem 0; margin: 0.5rem 0;
} }
/* Mobile-specific styles */
.mobile-only {
display: none;
}
@media (min-width: 769px) {
.mobile-only {
display: none !important;
}
}
.hamburger-button {
background: none;
border: none;
color: var(--text-primary);
cursor: pointer;
padding: 0.5rem;
border-radius: 0.375rem;
transition: background-color 0.2s ease;
display: flex;
align-items: center;
justify-content: center;
}
.hamburger-button:hover {
background-color: var(--bg-tertiary);
}
.mobile-menu {
background-color: var(--bg-secondary);
border-top: 1px solid var(--border-color);
padding: 1rem 0;
}
.mobile-nav-links {
display: flex;
flex-direction: column;
padding: 0 1.5rem;
}
.mobile-nav-links a {
display: flex;
align-items: center;
gap: 1rem;
padding: 1rem 0;
color: var(--text-secondary);
text-decoration: none;
font-weight: 500;
transition: color 0.2s ease;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}
.mobile-nav-links a:last-child {
border-bottom: none;
}
.mobile-nav-links a:hover,
.mobile-nav-links a.active {
color: var(--accent-color);
}
.mobile-nav-links .material-icons {
font-size: 1.25rem;
}
.mobile-profile-section {
margin-top: 1rem;
padding: 1rem 1.5rem 0;
border-top: 1px solid var(--border-color);
}
.mobile-user-info {
display: flex;
align-items: center;
gap: 1rem;
margin-bottom: 1rem;
}
.mobile-profile-image {
width: 2rem;
height: 2rem;
border-radius: 50%;
object-fit: cover;
border: 2px solid var(--accent-color);
}
.mobile-user-name {
color: var(--text-primary);
font-weight: 500;
}
.mobile-profile-divider {
height: 1px;
background-color: var(--border-color);
margin: 1rem 0;
}
.mobile-logout-button,
.mobile-login-button {
display: flex;
align-items: center;
gap: 1rem;
padding: 0.75rem 0;
background: none;
border: none;
color: var(--danger-color);
text-decoration: none;
font-size: 1rem;
cursor: pointer;
width: 100%;
text-align: left;
transition: color 0.2s ease;
}
.mobile-login-button {
color: var(--accent-color);
}
.mobile-logout-button:hover {
color: var(--danger-hover);
}
.mobile-login-button:hover {
color: var(--accent-hover);
}
/* Main content */ /* Main content */
main { main {
flex: 1; flex: 1;
@ -371,9 +563,10 @@
} }
.content-container { .content-container {
max-width: 1200px; max-width: 1250px;
margin: 0 auto; margin: 0 auto;
width: 100%; width: 100%;
overflow-y: visible;
} }
/* Footer */ /* Footer */
@ -455,60 +648,84 @@
/* Media queries */ /* Media queries */
@media (max-width: 768px) { @media (max-width: 768px) {
.navbar { /* Hide desktop navigation and profile */
display: flex; .desktop-nav,
flex-direction: column; .desktop-profile {
gap: 1rem; display: none;
padding: 1rem;
height: auto;
} }
.navbar-section { /* Show mobile hamburger */
width: 100%; .mobile-only {
justify-content: center; display: flex !important;
}
/* Adjust navbar layout for mobile */
.navbar {
grid-template-columns: auto 1fr auto;
padding: 0 1rem;
} }
.logo-section { .logo-section {
padding-right: 0; justify-content: flex-start;
overflow: hidden;
min-width: 0;
} }
.nav-links { .logo-svg {
width: 100%; height: 32px;
height: auto; max-width: calc(100vw - 8rem);
gap: 1rem; width: auto;
} }
.nav-links a { .hamburger-button {
height: auto; flex-shrink: 0;
padding: 0.5rem 0.75rem; justify-self: end;
} }
.nav-links a .hover-indicator { /* Additional breakpoint for very narrow screens like folded phones */
bottom: -4px; @media (max-width: 480px) {
.logo-svg {
height: 28px;
max-width: calc(100vw - 6rem);
}
.navbar {
padding: 0 0.75rem;
}
} }
.profile-container { @media (max-width: 360px) {
width: 100%; .logo-svg {
justify-content: center; height: 24px;
} max-width: calc(100vw - 5rem);
}
.profile-dropdown {
position: absolute; .navbar {
right: auto; padding: 0 0.5rem;
}
} }
/* Footer adjustments */
.footer-content { .footer-content {
grid-template-columns: 1fr; grid-template-columns: 1fr;
text-align: center; text-align: center;
} }
.footer-links, .contact-link { .footer-links {
align-items: center; align-items: center;
justify-content: center; justify-content: center;
} }
.contact-link {
justify-content: center;
}
} }
@media (min-width: 769px) and (max-width: 1024px) { @media (min-width: 769px) and (max-width: 1024px) {
.navbar {
padding: 0 1rem;
}
.footer-content { .footer-content {
grid-template-columns: 1fr 1fr; grid-template-columns: 1fr 1fr;
} }

View file

@ -31,7 +31,7 @@ export async function GET({ params }) {
headers: { headers: {
'Content-Type': 'text/markdown; charset=utf-8', 'Content-Type': 'text/markdown; charset=utf-8',
'Cache-Control': 'max-age=600', // Cache for 10 minutes 'Cache-Control': 'max-age=600', // Cache for 10 minutes
'Access-Control-Allow-Origin': '*' // Add CORS if needed 'Access-Control-Allow-Origin': '*' // Add CORS
} }
}); });
} }

View file

@ -17,145 +17,149 @@
category: "Getting Started", category: "Getting Started",
icon: "rocket_launch", icon: "rocket_launch",
items: [ items: [
{ title: "Introduction", file: "1-intro", order: 1 }, { title: "Introduction", file: "1-intro" },
{ title: "Installation", file: "1-installation", order: 2 }, { title: "Installation", file: "1-installation" },
{ title: "Familiarisation, States", file: "1-fam-states-intro", order: 3 }, { title: "Familiarisation, States", file: "1-fam-states-intro" },
{ title: "How TA Works, Info", file: "1-taInfo", order: 4 }, { title: "How TA Works, Info", file: "1-taInfo" },
] ]
}, },
{ {
category: "State Manager", category: "State Manager",
icon: "account_tree", icon: "account_tree",
items: [ items: [
{ title: "Introduction to the StateManager", file: "2-stateManager-intro", order: 1 }, { title: "Introduction to the StateManager", file: "2-stateManager-intro" },
{ title: "All Functions", file: "2-stateManager-allFunctions", order: 2 }, { title: "All Functions", file: "2-stateManager-allFunctions" },
{ title: "Emit", file: "2-stateManager-emit", order: 3 }, { title: "Emit", file: "2-stateManager-emit" },
{ title: "GetKnownServers", file: "2-stateManager-getKnownServers", order: 4 }, { title: "GetKnownServers", file: "2-stateManager-getKnownServers" },
{ title: "GetMatch", file: "2-stateManager-getMatch", order: 6 }, { title: "GetMatch", file: "2-stateManager-getMatch" },
{ title: "GetMatches", file: "2-stateManager-getMatches", order: 7 }, { title: "GetMatches", file: "2-stateManager-getMatches" },
{ title: "GetQualifier", file: "2-stateManager-getQualifier", order: 7 }, { title: "GetQualifier", file: "2-stateManager-getQualifier" },
{ title: "GetQualifiers", file: "2-stateManager-getQualifiers", order: 8 }, { title: "GetQualifiers", file: "2-stateManager-getQualifiers" },
{ title: "GetSelfGuid", file: "2-stateManager-getSelfGuid", order: 9 }, { title: "GetSelfGuid", file: "2-stateManager-getSelfGuid" },
{ title: "GetTournament", file: "2-stateManager-getTournament", order: 10 }, { title: "GetTournament", file: "2-stateManager-getTournament" },
{ title: "GetTournaments", file: "2-stateManager-getTournaments", order: 11 }, { title: "GetTournaments", file: "2-stateManager-getTournaments" },
{ title: "GetUser", file: "2-stateManager-getUser", order: 12 }, { title: "GetUser", file: "2-stateManager-getUser" },
{ title: "GetUsers", file: "2-stateManager-getUsers", order: 13 }, { title: "GetUsers", file: "2-stateManager-getUsers" },
{ title: "HandlePacket", file: "2-stateManager-handlePacket", order: 14 }, { title: "HandlePacket", file: "2-stateManager-handlePacket" },
{ title: "On", file: "2-stateManager-on", order: 15 }, { title: "On", file: "2-stateManager-on" },
{ title: "Once", file: "2-stateManager-once", order: 16 }, { title: "Once", file: "2-stateManager-once" },
{ title: "RemoveListener", file: "2-stateManager-removeListener", order: 17 }, { title: "RemoveListener", file: "2-stateManager-removeListener" },
] ]
}, },
{ {
category: "Client", category: "Client",
icon: "api", icon: "api",
items: [ items: [
{ title: "Introduction to the Client", file: "3-client-intro", order: 1 }, { title: "Introduction to the Client", file: "3-client-intro" },
{ title: "All Functions", file: "3-client-allFunctions", order: 2 }, { title: "All Functions", file: "3-client-allFunctions" },
{ title: "AddAuthorizedUser", file: "3-client-addAuthorizedUser", order: 3 }, { title: "AddAuthorizedUser", file: "3-client-addAuthorizedUser" },
{ title: "AddQualifierMaps", file: "3-client-addQualifierMaps", order: 4 }, { title: "AddQualifierMaps", file: "3-client-addQualifierMaps" },
{ title: "AddServer", file: "3-client-addServer", order: 5 }, { title: "AddServer", file: "3-client-addServer" },
{ title: "AddTournamentPool", file: "3-client-addTournamentPool", order: 6 }, { title: "AddTournamentPool", file: "3-client-addTournamentPool" },
{ title: "AddTournamentPoolMaps", file: "3-client-addTournamentPoolMaps", order: 7 }, { title: "AddTournamentPoolMaps", file: "3-client-addTournamentPoolMaps" },
{ title: "AddTournamentTeam", file: "3-client-addTournamentTeam", order: 8 }, { title: "AddTournamentRole", file: "3-client-addTournamentRole" },
{ title: "AddUserToMatch", file: "3-client-addUserToMatch", order: 9 }, { title: "AddTournamentTeam", file: "3-client-addTournamentTeam" },
{ title: "Connect", file: "3-client-connect", order: 10 }, { title: "AddUserToMatch", file: "3-client-addUserToMatch" },
{ title: "CreateMatch", file: "3-client-createMatch", order: 11 }, { title: "Connect", file: "3-client-connect" },
{ title: "CreateQualifierEvent", file: "3-client-createQualifierEvent", order: 12 }, { title: "CreateMatch", file: "3-client-createMatch" },
{ title: "CreateTournament", file: "3-client-createTournament", order: 13 }, { title: "CreateQualifierEvent", file: "3-client-createQualifierEvent" },
{ title: "DelayTestFinished", file: "3-client-delayTestFinished", order: 14 }, { title: "CreateTournament", file: "3-client-createTournament" },
{ title: "DeleteMatch", file: "3-client-deleteMatch", order: 15 }, { title: "DelayTestFinished", file: "3-client-delayTestFinished" },
{ title: "DeleteQualifierEvent", file: "3-client-deleteQualifierEvent", order: 16 }, { title: "DeleteMatch", file: "3-client-deleteMatch" },
{ title: "DeleteTournament", file: "3-client-deleteTournament", order: 17 }, { title: "DeleteQualifierEvent", file: "3-client-deleteQualifierEvent" },
{ title: "Disconnect", file: "3-client-disconnect", order: 18 }, { title: "DeleteTournament", file: "3-client-deleteTournament" },
{ title: "Emit", file: "3-client-emit", order: 19 }, { title: "Disconnect", file: "3-client-disconnect" },
{ title: "FlipColors", file: "3-client-flipColors", order: 20 }, { title: "Emit", file: "3-client-emit" },
{ title: "FlipHands", file: "3-client-flipHands", order: 21 }, { title: "FlipColors", file: "3-client-flipColors" },
{ title: "GenerateBotToken", file: "3-client-generateBotToken", order: 22 }, { title: "FlipHands", file: "3-client-flipHands" },
{ title: "GetAuthorizedUsers", file: "3-client-getAuthorizedUsers", order: 23 }, { title: "GenerateBotToken", file: "3-client-generateBotToken" },
{ title: "GetBotTokensForUser", file: "3-client-getBotTokensForUser", order: 24 }, { title: "GetAuthorizedUsers", file: "3-client-getAuthorizedUsers" },
{ title: "GetDiscordInfo", file: "3-client-getDiscordInfo", order: 25 }, { title: "GetBotTokensForUser", file: "3-client-getBotTokensForUser" },
{ title: "GetLeaderboard", file: "3-client-getLeaderboard", order: 26 }, { title: "GetDiscordInfo", file: "3-client-getDiscordInfo" },
{ title: "IsConnected", file: "3-client-isConnected", order: 27 }, { title: "GetLeaderboard", file: "3-client-getLeaderboard" },
{ title: "IsConnecting", file: "3-client-isConnecting", order: 28 }, { title: "IsConnected", file: "3-client-isConnected" },
{ title: "JoinTournament", file: "3-client-joinTournament", order: 29 }, { title: "IsConnecting", file: "3-client-isConnecting" },
{ title: "LoadImage", file: "3-client-loadImage", order: 30 }, { title: "JoinTournament", file: "3-client-joinTournament" },
{ title: "LoadSong", file: "3-client-loadSong", order: 31 }, { title: "LoadImage", file: "3-client-loadImage" },
{ title: "On", file: "3-client-on", order: 32 }, { title: "LoadSong", file: "3-client-loadSong" },
{ title: "Once", file: "3-client-once", order: 33 }, { title: "On", file: "3-client-on" },
{ title: "PlaySong", file: "3-client-playSong", order: 34 }, { title: "Once", file: "3-client-once" },
{ title: "RemoveAuthorizedUser", file: "3-client-removeAuthorizedUser", order: 35 }, { title: "PlaySong", file: "3-client-playSong" },
{ title: "RemoveListener", file: "3-client-removeListener", order: 36 }, { title: "RemoveAuthorizedUser", file: "3-client-removeAuthorizedUser" },
{ title: "RemoveQualifierMap", file: "3-client-removeQualifierMap", order: 37 }, { title: "RemoveListener", file: "3-client-removeListener" },
{ title: "RemoveTournamentPool", file: "3-client-removeTournamentPool", order: 38 }, { title: "RemoveQualifierMap", file: "3-client-removeQualifierMap" },
{ title: "RemoveTournamentPoolMap", file: "3-client-removeTournamentPoolMap", order: 39 }, { title: "RemoveTournamentPool", file: "3-client-removeTournamentPool" },
{ title: "RemoveTournamentTeam", file: "3-client-removeTournamentTeam", order: 40 }, { title: "RemoveTournamentPoolMap", file: "3-client-removeTournamentPoolMap" },
{ title: "RemoveUserFromMatch", file: "3-client-removeUserFromMatch", order: 41 }, { title: "RemoveTournamentRole", file: "3-client-removeTournamentRole" },
{ title: "ReturnToMenu", file: "3-client-returnToMenu", order: 42 }, { title: "RemoveTournamentTeam", file: "3-client-removeTournamentTeam" },
{ title: "RevokeBotToken", file: "3-client-revokeBotToken", order: 43 }, { title: "RemoveUserFromMatch", file: "3-client-removeUserFromMatch" },
{ title: "SendResponse", file: "3-client-sendResponse", order: 44 }, { title: "ReturnToMenu", file: "3-client-returnToMenu" },
{ title: "SetAuthToken", file: "3-client-setAuthToken", order: 45 }, { title: "RevokeBotToken", file: "3-client-revokeBotToken" },
{ title: "SetMatchLeader", file: "3-client-setMatchLeader", order: 46 }, { title: "SendResponse", file: "3-client-sendResponse" },
{ title: "SetMatchMap", file: "3-client-setMatchMap", order: 47 }, { title: "SetAuthToken", file: "3-client-setAuthToken" },
{ title: "SetQualifierFlags", file: "3-client-setQualifierFlags", order: 48 }, { title: "SetMatchLeader", file: "3-client-setMatchLeader" },
{ title: "SetQualifierImage", file: "3-client-setQualifierImage", order: 49 }, { title: "SetMatchMap", file: "3-client-setMatchMap" },
{ title: "SetQualifierInfoChannel", file: "3-client-setQualifierInfoChannel", order: 50 }, { title: "SetQualifierFlags", file: "3-client-setQualifierFlags" },
{ title: "SetQualifierLeaderboardSort", file: "3-client-setQualifierLeaderboardSort", order: 51 }, { title: "SetQualifierImage", file: "3-client-setQualifierImage" },
{ title: "SetQualifierName", file: "3-client-setQualifierName", order: 52 }, { title: "SetQualifierInfoChannel", file: "3-client-setQualifierInfoChannel" },
{ title: "SetTournamentAllowUnauthorizedView", file: "3-client-setTournamentAllowUnauthorizedView", order: 53 }, { title: "SetQualifierLeaderboardSort", file: "3-client-setQualifierLeaderboardSort" },
{ title: "SetTournamentBannedMods", file: "3-client-setTournamentBannedMods", order: 54 }, { title: "SetQualifierName", file: "3-client-setQualifierName" },
{ title: "SetTournamentEnablePools", file: "3-client-setTournamentEnablePools", order: 55 }, { title: "SetTournamentAllowUnauthorizedView", file: "3-client-setTournamentAllowUnauthorizedView" },
{ title: "SetTournamentEnableTeams", file: "3-client-setTournamentEnableTeams", order: 56 }, { title: "SetTournamentBannedMods", file: "3-client-setTournamentBannedMods" },
{ title: "SetTournamentImage", file: "3-client-setTournamentImage", order: 57 }, { title: "SetTournamentEnablePools", file: "3-client-setTournamentEnablePools" },
{ title: "SetTournamentName", file: "3-client-setTournamentName", order: 58 }, { title: "SetTournamentEnableTeams", file: "3-client-setTournamentEnableTeams" },
{ title: "SetTournamentPoolName", file: "3-client-setTournamentPoolName", order: 59 }, { title: "SetTournamentImage", file: "3-client-setTournamentImage" },
{ title: "SetTournamentScoreUpdateFrequency", file: "3-client-setTournamentScoreUpdateFrequency", order: 60 }, { title: "SetTournamentName", file: "3-client-setTournamentName" },
{ title: "SetTournamentShowQualifierButton", file: "3-client-setTournamentShowQualifierButton", order: 61 }, { title: "SetTournamentPoolName", file: "3-client-setTournamentPoolName" },
{ title: "SetTournamentShowTournamentButton", file: "3-client-setTournamentShowTournamentButton", order: 62 }, { title: "SetTournamentRoleName", file: "3-client-setTournamentRoleName" },
{ title: "SetTournamentTeamImage", file: "3-client-setTournamentTeamImage", order: 63 }, { title: "SetTournamentRolePermissions", file: "3-client-setTournamentRolePermissions" },
{ title: "SetTournamentTeamName", file: "3-client-setTournamentTeamName", order: 64 }, { title: "SetTournamentScoreUpdateFrequency", file: "3-client-setTournamentScoreUpdateFrequency" },
{ title: "ShowLoadedImage", file: "3-client-showLoadedImage", order: 65 }, { title: "SetTournamentShowQualifierButton", file: "3-client-setTournamentShowQualifierButton" },
{ title: "ShowPrompt", file: "3-client-showPrompt", order: 66 }, { title: "SetTournamentShowTournamentButton", file: "3-client-setTournamentShowTournamentButton" },
{ title: "StateManager", file: "3-client-stateManager", order: 67 }, { title: "SetTournamentTeamImage", file: "3-client-setTournamentTeamImage" },
{ title: "UpdateQualifierMap", file: "3-client-updateQualifierMap", order: 68 }, { title: "SetTournamentTeamName", file: "3-client-setTournamentTeamName" },
{ title: "UpdateTournamentPoolMap", file: "3-client-updateTournamentPoolMap", order: 69 }, { title: "ShowLoadedImage", file: "3-client-showLoadedImage" },
{ title: "UpdateUser", file: "3-client-updateUser", order: 70 } { title: "ShowPrompt", file: "3-client-showPrompt" },
{ title: "StateManager", file: "3-client-stateManager" },
{ title: "UpdateQualifierMap", file: "3-client-updateQualifierMap" },
{ title: "UpdateTournamentPoolMap", file: "3-client-updateTournamentPoolMap" },
{ title: "UpdateUser", file: "3-client-updateUser" }
] ]
}, },
{ {
category: "Models", category: "Models",
icon: "view_in_ar", icon: "view_in_ar",
items: [ items: [
{ title: "4-", file: "", order: 1 }, { title: "4-", file: "" },
{ title: "Endpoints", file: "", order: 2 }, { title: "Endpoints", file: "" },
{ title: "Response Types", file: "", order: 3 } { title: "Response Types", file: "" }
] ]
}, },
{ {
category: "Enums", category: "Enums",
icon: "view_in_ar", icon: "view_in_ar",
items: [ items: [
{ title: "5-", file: "", order: 1 }, { title: "5-", file: "" },
{ title: "Endpoints", file: "", order: 2 }, { title: "Endpoints", file: "" },
{ title: "Response Types", file: "", order: 3 } { title: "Response Types", file: "" }
] ]
}, },
{ {
category: "Events", category: "Events",
icon: "view_in_ar", icon: "view_in_ar",
items: [ items: [
{ title: "6-", file: "", order: 1 }, { title: "6-", file: "" },
{ title: "Endpoints", file: "", order: 2 }, { title: "Endpoints", file: "" },
{ title: "Response Types", file: "", order: 3 } { title: "Response Types", file: "" }
] ]
}, },
{ {
category: "Best Practices", category: "Best Practices",
icon: "view_in_ar", icon: "view_in_ar",
items: [ items: [
{ title: "7-", file: "", order: 1 }, { title: "7-", file: "" },
{ title: "Endpoints", file: "", order: 2 }, { title: "Endpoints", file: "" },
{ title: "Response Types", file: "", order: 3 } { title: "Response Types", file: "" }
] ]
} }
]; ];
@ -241,7 +245,7 @@
// Find first doc in category and set it as current // Find first doc in category and set it as current
const categoryData = docStructure.find(c => c.category === category); const categoryData = docStructure.find(c => c.category === category);
if (categoryData && categoryData.items.length > 0) { if (categoryData && categoryData.items.length > 0) {
categoryData.items.sort((a, b) => a.order - b.order); categoryData.items.sort((a, b) => categoryData.items.indexOf(a) - categoryData.items.indexOf(b));
currentDoc = categoryData.items[0].file; currentDoc = categoryData.items[0].file;
window.location.hash = `${encodeURIComponent(category)}/${encodeURIComponent(currentDoc)}`; window.location.hash = `${encodeURIComponent(category)}/${encodeURIComponent(currentDoc)}`;
} }
@ -407,7 +411,7 @@
{#if activeCategory === category.category} {#if activeCategory === category.category}
<!-- svelte-ignore missing-declaration --> <!-- svelte-ignore missing-declaration -->
<div class="category-items" transition:slide={{duration: 200}}> <div class="category-items" transition:slide={{duration: 200}}>
{#each category.items.sort((a, b) => a.order - b.order) as item} {#each category.items.sort((a, b) => category.items.indexOf(a) - category.items.indexOf(b)) as item}
<button <button
class="doc-link" class="doc-link"
class:active={currentDoc === item.file} class:active={currentDoc === item.file}
@ -491,7 +495,7 @@
/* Sidebar styles */ /* Sidebar styles */
.sidebar { .sidebar {
width: 280px; width: 330px;
background-color: var(--bg-secondary); background-color: var(--bg-secondary);
border-right: 1px solid var(--border-color); border-right: 1px solid var(--border-color);
height: 100%; height: 100%;
@ -923,6 +927,7 @@
display: flex; display: flex;
gap: 1rem; gap: 1rem;
align-items: flex-start; align-items: flex-start;
color: var(--text-primary);
} }
.markdown-container :global(.info-box) { .markdown-container :global(.info-box) {

View file

@ -4,12 +4,13 @@
import { bkAPIUrl } from '$lib/config.json'; import { bkAPIUrl } from '$lib/config.json';
import AddNewAuthorisedUser from "$lib/components/popups/AddNewAuthorisedUser.svelte"; import AddNewAuthorisedUser from "$lib/components/popups/AddNewAuthorisedUser.svelte";
//@ts-ignore //@ts-ignore
import { Match, Tournament, TAClient, Response_ResponseType } from 'moons-ta-client'; import { Match, Tournament, TAClient, Response_ResponseType, Role } from 'moons-ta-client';
import { writable } from "svelte/store"; import { writable } from "svelte/store";
import SideMenu from "$lib/components/menus/SideMenuTournaments.svelte"; import SideMenu from "$lib/components/menus/SideMenuTournaments.svelte";
import InfoPopup from "$lib/components/notifications/InfoPopup.svelte"; import InfoPopup from "$lib/components/notifications/InfoPopup.svelte";
import { bufferToImageUrl, convertImageToUint8Array, linkToUint8Array } from "$lib/services/taImages.js"; import { bufferToImageUrl, convertImageToUint8Array, linkToUint8Array } from "$lib/services/taImages.js";
import Popup from "$lib/components/notifications/Popup.svelte"; import Popup from "$lib/components/notifications/Popup.svelte";
import AddOrModifyRole from "$lib/components/popups/AddOrModifyRole.svelte";
import { goto } from "$app/navigation"; import { goto } from "$app/navigation";
export let data; export let data;
@ -37,6 +38,11 @@
let popupContent: string = ""; let popupContent: string = "";
let showAddUserPopup = false; let showAddUserPopup = false;
let showSuccessfullySaved = false; let showSuccessfullySaved = false;
let tournamentRoles: Role[] = [];
// Role popup state
let showRolePopup: boolean = false;
let editingRole: Role | null = null;
// File upload // File upload
let fileInput: HTMLInputElement; let fileInput: HTMLInputElement;
@ -54,13 +60,6 @@
scoreUpdateFrequency: false, scoreUpdateFrequency: false,
bannedMods: false bannedMods: false
} }
enum Permissions {
"None" = 0,
"View Only" = 1,
"Administrator" = 2,
"View and Administrator" = 3
}
// Remove 'Bearer ' from the token // Remove 'Bearer ' from the token
if($authTokenStore) { if($authTokenStore) {
@ -125,7 +124,7 @@
console.log('Adding user:', userData); console.log('Adding user:', userData);
try { try {
let addAuthorizedUserResult = await client.addAuthorizedUser(tournamentGuid, userData.discordId, userData.permission); let addAuthorizedUserResult = await client.addAuthorizedUser(tournamentGuid, userData.discordId, userData.roleIds);
// If successful, add to the authorizedUsers array // If successful, add to the authorizedUsers array
if (addAuthorizedUserResult.type === Response_ResponseType.Success) { if (addAuthorizedUserResult.type === Response_ResponseType.Success) {
@ -137,6 +136,65 @@
} }
} }
async function handleAddRole(event: CustomEvent) {
const roleData = event.detail;
console.log('Adding role:', roleData);
let role: Role = {
name: roleData.name,
roleId: roleData.roleId,
permissions: roleData.permisions,
tournamentId: tournamentGuid,
guid: ''
}
try {
let addRoleResult = await client.addTournamentRole(tournamentGuid, role);
console.log("result", addRoleResult)
if (addRoleResult.type === Response_ResponseType.Success) {
tournamentRoles = [...tournamentRoles, role];
}
} catch (err) {
console.error('Error adding role:', err);
error = "Failed to add role, please view console, screenshot it and send it to serverbp or matrikmoon on Discord.";
}
}
async function handleModifyRole(event: CustomEvent) {
const roleData = event.detail;
console.log('Adding role:', roleData);
const role: Role = {
name: roleData.name,
roleId: roleData.roleId,
permissions: roleData.permisions,
tournamentId: tournamentGuid,
guid: ''
}
try {
let modifyRoleName = await client.setTournamentRoleName(tournamentGuid, roleData.roleId, roleData.name);
let modifyRolePermissions = await client.setTournamentRolePermissions(tournamentGuid, roleData.roleId, roleData.permissions);
// If successful, add to the authorizedUsers array
if (modifyRoleName.type === Response_ResponseType.Success && modifyRolePermissions.type === Response_ResponseType.Success) {
tournamentRoles = [role, ...tournamentRoles.filter(x => x.roleId !== role.roleId)];
}
} catch (err) {
console.error('Error modifying role:', err);
error = "Failed to modify role, please view console, screenshot it and send it to serverbp or matrikmoon on Discord.";
}
}
async function handleDeleteRole(roleId: string): Promise<any> {
const removeRoleResponse = await client.removeTournamentRole(tournamentGuid, roleId);
if (removeRoleResponse.type === Response_ResponseType.Success) {
console.log("Removed role", removeRoleResponse);
tournamentRoles = [...tournamentRoles.filter(x => x.roleId !== roleId)];
}
}
function removeImage() { function removeImage() {
imagePreview = "/talogo.png"; imagePreview = "/talogo.png";
tournamentImage = "/talogo.png"; tournamentImage = "/talogo.png";
@ -146,6 +204,15 @@
function openAddUserPopup() { function openAddUserPopup() {
showAddUserPopup = true; showAddUserPopup = true;
} }
function handleEditRole(roleGuid: string): any {
editingRole = tournamentRoles.find(x => x.guid == roleGuid)!;
showRolePopup = true;
}
function openAddOrModifyRolePopup() {
showRolePopup = true;
}
function closeAddUserPopup() { function closeAddUserPopup() {
showAddUserPopup = false; showAddUserPopup = false;
@ -251,31 +318,13 @@
hideTournament = !tournament.settings?.allowUnauthorizedView || false; hideTournament = !tournament.settings?.allowUnauthorizedView || false;
scoreUpdateFrequency = tournament.settings?.scoreUpdateFrequency || 1; scoreUpdateFrequency = tournament.settings?.scoreUpdateFrequency || 1;
bannedMods = tournament.settings?.bannedMods || []; bannedMods = tournament.settings?.bannedMods || [];
tournamentRoles = tournament.settings?.roles || [];
console.log("roles", tournamentRoles)
// For demo purposes, populate authorised users
// This would typically come from the tournament object
let authorisedUserRes = await client.getAuthorizedUsers(tournamentGuid); let authorisedUserRes = await client.getAuthorizedUsers(tournamentGuid);
console.log(authorisedUserRes) console.log(authorisedUserRes)
authorizedUsers = (authorisedUserRes as any).details.getAuthorizedUsers.authorizedUsers.filter((user: any) => user.discordId !== $discordDataStore.id) || [ authorizedUsers = (authorisedUserRes as any).details.getAuthorizedUsers.authorizedUsers.filter((user: any) => user.discordId !== $discordDataStore.id)
{
guid: "user1",
name: "Failed Data",
discordInfo: {
username: "TourneyAdmin",
avatarUrl: "https://api.dicebear.com/7.x/avataaars/svg?seed=admin"
},
permissions: "admin"
},
{
guid: "user2",
name: "Failed Data",
discordInfo: {
username: "TourneyViewer",
avatarUrl: "https://api.dicebear.com/7.x/avataaars/svg?seed=viewer"
},
permissions: "view only"
}
];
} }
} catch (err) { } catch (err) {
@ -465,7 +514,7 @@
<!-- Advanced Settings Section --> <!-- Advanced Settings Section -->
<div class="settings-section"> <div class="settings-section">
<h3>Advanced Settings</h3> <h3>Scoring Settings</h3>
<div class="settings-group"> <div class="settings-group">
<div class="setting-row"> <div class="setting-row">
@ -519,6 +568,35 @@
</div> </div>
</div> </div>
</div> </div>
<div class="settings-section">
<h3>Authorisation Roles</h3>
<div class="settings-group">
<div class="actions">
<button class="action-button add-user" on:click={openAddOrModifyRolePopup}>
<span class="material-icons">add_moderator</span>
Add Role
</button>
</div>
<div class="user-cards-grid">
{#each tournamentRoles as role}
<div class="user-card">
<div class="user-info">
<h4>{role.name || "Unknown Role"}</h4>
</div>
<button class="edit-button" on:click={handleEditRole(role.guid)}>
<span class="material-icons">edit</span>
</button>
<button class="trash-button" on:click={async() => await handleDeleteRole(role.roleId)}>
<span class="material-icons">delete</span>
</button>
</div>
{/each}
</div>
</div>
</div>
</div> </div>
<div class="settings-section full-width"> <div class="settings-section full-width">
@ -526,10 +604,13 @@
<div class="settings-group"> <div class="settings-group">
<div class="authorized-users-container"> <div class="authorized-users-container">
<button class="action-button add-user" on:click={openAddUserPopup}> <div class="actions">
<span class="material-icons">person_add</span> <button class="action-button add-user" on:click={openAddUserPopup}>
Add User <span class="material-icons">person_add</span>
</button> Add User
</button>
</div>
{#if authorizedUsers.length === 0} {#if authorizedUsers.length === 0}
<p class="no-users">No authorized users added yet</p> <p class="no-users">No authorized users added yet</p>
{:else} {:else}
@ -543,7 +624,10 @@
/> />
<div class="user-info"> <div class="user-info">
<h4>{user.name || user.discordUsername || "Unknown User"}</h4> <h4>{user.name || user.discordUsername || "Unknown User"}</h4>
<p class="user-permission">{Permissions[user.permission]}</p> {#each user.roles as roleId}
<p class="user-permission">{tournamentRoles.find(x => x.roleId == roleId)?.name}</p>
{/each}
</div> </div>
<button class="remove-user" on:click={() => removeAuthorisedUser(user.discordId)}> <button class="remove-user" on:click={() => removeAuthorisedUser(user.discordId)}>
<span class="material-icons">delete</span> <span class="material-icons">delete</span>
@ -562,10 +646,18 @@
isOpen={showAddUserPopup} isOpen={showAddUserPopup}
tournamentGuid={tournamentGuid} tournamentGuid={tournamentGuid}
taClient={client} taClient={client}
tournamentRoles={tournamentRoles}
on:close={closeAddUserPopup} on:close={closeAddUserPopup}
on:addUser={handleAddUser} on:addUser={handleAddUser}
/> />
<AddOrModifyRole
bind:isOpen={showRolePopup}
role={editingRole}
on:createRole={handleAddRole}
on:modifyRole={handleModifyRole}
/>
<Popup <Popup
bind:open={showSuccessfullySaved} bind:open={showSuccessfullySaved}
message="Successfully saved the tournament information!" message="Successfully saved the tournament information!"
@ -722,6 +814,58 @@
.info-button:hover { .info-button:hover {
color: var(--accent-hover); color: var(--accent-hover);
} }
/* Edit Button */
.edit-button {
background: none;
border: none;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
color: var(--accent-color);
padding: 0;
font-size: 0.875rem;
}
.edit-button .material-icons {
font-size: 1.125rem;
}
.edit-button:hover {
color: var(--accent-hover);
}
/* Trash Button */
.trash-button {
background: none;
border: none;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
color: #ff5555;
padding: 0.3rem;
font-size: 0.875rem;
border-radius: 30%;
}
.trash-button .material-icons {
font-size: 1.125rem;
}
.trash-button:hover {
background-color: rgba(255, 85, 85, 0.1);
}
.role-item {
display: flex;
align-items: center;
background-color: var(--bg-tertiary);
border-radius: 0.5rem;
padding: 0.3rem;
padding-left: 1rem;
}
/* Image upload */ /* Image upload */
.image-upload-container { .image-upload-container {
@ -890,7 +1034,7 @@
} }
.action-button.add-user { .action-button.add-user {
align-self: flex-start; /* align-self: flex-start; */
background-color: var(--accent-color); background-color: var(--accent-color);
border: none; border: none;
border-radius: 0.375rem; border-radius: 0.375rem;