diff --git a/MCTournamentSystem-Bungeecord/src/main/java/net/novauniverse/mctournamentsystem/bungeecord/api/handlers/api/v1/snapshot/ImportSnapshotHandler.java b/MCTournamentSystem-Bungeecord/src/main/java/net/novauniverse/mctournamentsystem/bungeecord/api/handlers/api/v1/snapshot/ImportSnapshotHandler.java
index 0ba785c..dc0acfd 100644
--- a/MCTournamentSystem-Bungeecord/src/main/java/net/novauniverse/mctournamentsystem/bungeecord/api/handlers/api/v1/snapshot/ImportSnapshotHandler.java
+++ b/MCTournamentSystem-Bungeecord/src/main/java/net/novauniverse/mctournamentsystem/bungeecord/api/handlers/api/v1/snapshot/ImportSnapshotHandler.java
@@ -106,7 +106,7 @@ public AbstractHTTPResponse handleRequest(Request request, Authentication authen
playerIds.stream().filter(p -> p.getUuid().toString().equalsIgnoreCase(uuidString)).findFirst().ifPresent(pid -> {
try {
- String server = player.getString("server");
+ String server = player.optString("server", "");
String reason = player.getString("reason");
int amount = player.getInt("amount");
String gainedAt = player.getString("gained_at");
@@ -136,7 +136,7 @@ public AbstractHTTPResponse handleRequest(Request request, Authentication authen
teamIds.stream().filter(t -> t.getTeamNumber() == teamNumber).findFirst().ifPresent(tid -> {
try {
- String server = team.getString("server");
+ String server = team.optString("server", "");
String reason = team.getString("reason");
int amount = team.getInt("amount");
String gainedAt = team.getString("gained_at");
diff --git a/ReactUI/src/App.tsx b/ReactUI/src/App.tsx
index 962a85b..54bc7c8 100644
--- a/ReactUI/src/App.tsx
+++ b/ReactUI/src/App.tsx
@@ -22,6 +22,7 @@ import EditorProvider from './components/EditorProvider';
/// @ts-ignore
import catCry from "./assets/img/cat_cry.png";
+import ScoreSnapshot from './pages/ScoreSnapshot';
export default function App() {
const tournamentSystem = useTournamentSystemContext();
@@ -63,6 +64,7 @@ export default function App() {
} />
} />
} />
+ } />
{/* Unauthenticated zones */}
} />
diff --git a/ReactUI/src/components/modals/ServerSelector.tsx b/ReactUI/src/components/modals/ServerSelector.tsx
index 1fa5581..7b3ca40 100644
--- a/ReactUI/src/components/modals/ServerSelector.tsx
+++ b/ReactUI/src/components/modals/ServerSelector.tsx
@@ -32,7 +32,7 @@ export default function ServerSelector({ visible, text, title = "Select server",
}, []);
useEffect(() => {
- console.debug("Resetting server picker modal");
+ //console.debug("Resetting server picker modal");
if (servers.length > 0) {
setServer(servers[0].name);
}
diff --git a/ReactUI/src/components/modals/TextPromptModal.tsx b/ReactUI/src/components/modals/TextPromptModal.tsx
index bc69bb4..e778b3f 100644
--- a/ReactUI/src/components/modals/TextPromptModal.tsx
+++ b/ReactUI/src/components/modals/TextPromptModal.tsx
@@ -15,17 +15,18 @@ interface Props {
extraButtonText?: string;
extraButtonType?: string;
allowEnterToSubmit?: boolean;
+ initialValue?: string;
onExtraButtonClick?: () => void;
onClose: () => void;
onSubmit: (text: string) => void;
}
-export default function TextPromptModal({ maxLength, placeholder, children, visible, title, onClose, onSubmit, allowEnterToSubmit = true, cancelText = "Cancel", confirmText = "Confirm", extraButtonVisible = false, extraButtonText = "Extra button", extraButtonType = "secondary", onExtraButtonClick = () => { }, cancelType = "secondary", confirmType = "primary" }: Props) {
+export default function TextPromptModal({ initialValue = "", maxLength, placeholder, children, visible, title, onClose, onSubmit, allowEnterToSubmit = true, cancelText = "Cancel", confirmText = "Confirm", extraButtonVisible = false, extraButtonText = "Extra button", extraButtonType = "secondary", onExtraButtonClick = () => { }, cancelType = "secondary", confirmType = "primary" }: Props) {
const [text, setText] = useState("");
useEffect(() => {
- console.debug("Resetting text prompt modal");
- setText("");
+ //console.debug("Resetting text prompt modal");
+ setText(initialValue);
}, [visible]);
diff --git a/ReactUI/src/components/modals/console/ServerConsoleModal.tsx b/ReactUI/src/components/modals/console/ServerConsoleModal.tsx
index 9b13014..a196e1a 100644
--- a/ReactUI/src/components/modals/console/ServerConsoleModal.tsx
+++ b/ReactUI/src/components/modals/console/ServerConsoleModal.tsx
@@ -13,6 +13,7 @@ import "./ServerConsoleModal.scss";
import StartServerButton from '../../buttons/server/StartServerButton';
import KillServerButton from '../../buttons/server/KillServerButton';
import toast from 'react-hot-toast';
+import { Permission } from '../../../scripts/enum/Permission';
interface Props {
visible: boolean;
@@ -178,7 +179,7 @@ export default function ServerConsoleModal({ server, visible, onClose }: Props)
}
async function executeCommand() {
- if(command.trim().length == 0) {
+ if (command.trim().length == 0) {
return;
}
@@ -216,8 +217,8 @@ export default function ServerConsoleModal({ server, visible, onClose }: Props)
-
-
+
+
{server.is_running ?
:
diff --git a/ReactUI/src/components/nav/PageSelection.tsx b/ReactUI/src/components/nav/PageSelection.tsx
index 86a1c27..5ac1992 100644
--- a/ReactUI/src/components/nav/PageSelection.tsx
+++ b/ReactUI/src/components/nav/PageSelection.tsx
@@ -7,6 +7,7 @@ export default function PageSelection() {
+ { setNameModalOpen(false) }} initialValue={tournamentSystem.state.system.tournament_name} onSubmit={onSetName} title='Set tournament name' visible={nameModalOpen} cancelText='Cancel' cancelType='secondary' confirmType='primary' confirmText='Set name' placeholder='Tournament name'>
+
+ Enter the new tournament name
+
+
+
+ { setMOTDModalOpen(false) }} initialValue={tournamentSystem.state.system.motd} onSubmit={onSetMOTD} title='Set MOTD' visible={motdModalOpen} cancelText='Cancel' cancelType='secondary' confirmType='primary' confirmText='Set MOTD' placeholder='MOTD'>
+
+ Enter the new MOTD
+
+
+
+ { setUrlModalOpen(false) }} initialValue={tournamentSystem.state.system.scoreboard_url} onSubmit={onSetUrl} title='Set scoreboard url' visible={urlModalOpen} cancelText='Cancel' cancelType='secondary' confirmType='primary' confirmText='Set URL' placeholder='Scoreboard URL'>
+
+ Enter the new scoreboard url
+
+
+
>
)
diff --git a/ReactUI/src/pages/ScoreSnapshot.tsx b/ReactUI/src/pages/ScoreSnapshot.tsx
new file mode 100644
index 0000000..dde46f1
--- /dev/null
+++ b/ReactUI/src/pages/ScoreSnapshot.tsx
@@ -0,0 +1,106 @@
+import React, { ChangeEvent, useState } from 'react'
+import { Button, Col, Container, FormControl, FormGroup, FormLabel, Row } from 'react-bootstrap'
+import { useTournamentSystemContext } from '../context/TournamentSystemContext';
+import toast from 'react-hot-toast';
+import PageSelection from '../components/nav/PageSelection';
+import { Permission } from '../scripts/enum/Permission';
+
+export default function ScoreSnapshot() {
+ const tournamentSystem = useTournamentSystemContext();
+
+ const [data, setData] = useState("");
+
+ function handleDataChange(e: ChangeEvent) {
+ setData(e.target.value);
+ }
+
+ function handleFileChange(e: ChangeEvent) {
+ const file = e.target.files[0];
+
+ if (file) {
+ const reader = new FileReader();
+ reader.onload = (r) => {
+ const content = r.target!.result;
+ setData(String(content));
+ toast.success("File loaded");
+ };
+ reader.readAsText(file);
+ }
+ };
+
+ async function exportData() {
+ const data = await tournamentSystem.api.exportScoreSnapshot();
+
+ if (data.success) {
+ let dataStr = "data:text/json;charset=utf-8," + encodeURIComponent(JSON.stringify(data.data, null, 4));
+ let downloadAnchorNode = document.createElement('a');
+ downloadAnchorNode.setAttribute("href", dataStr);
+ downloadAnchorNode.setAttribute("download", "TournamentScoreSnapshot.json");
+ document.body.appendChild(downloadAnchorNode); // required for firefox
+ downloadAnchorNode.click();
+ downloadAnchorNode.remove();
+
+ toast.success("Score snapshot downloaded");
+ } else {
+ console.error("Failed to export snapshot. " + data.message);
+ toast.error("Failed to export snapshot. " + data.message);
+ }
+ }
+
+ async function importData() {
+ if (data.trim().length == 0) {
+ toast.error("Pleas paste JSON or import snapshot file first");
+ return;
+ }
+
+ let json: any;
+ try {
+ json = JSON.parse(data);
+ } catch (err) {
+ console.log("Failed to parse json");
+ console.error(err);
+ toast.error("Failed to parse data. Please check that the input is valid json and try again");
+ return;
+ }
+
+ if (!Array.isArray(json.players) || !Array.isArray(json.teams)) {
+ toast.error("The provided data does not seem to be valid score anspshot data");
+ return;
+ }
+
+ const response = await tournamentSystem.api.importScoreSnapshot(json);
+ if (response.success) {
+ toast.success("Score imported");
+ } else {
+ console.error("Failed to import score snapshot: " + response.message);
+ toast.error("Failed to import score snapshot, please verify that valid data was provided. " + response.message);
+ }
+ }
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Pase JSON here
+
+
+
+
+ Or upload JSON file with score data
+
+
+
+
+
+ )
+}
diff --git a/ReactUI/src/scripts/api/TournamentSystemAPI.ts b/ReactUI/src/scripts/api/TournamentSystemAPI.ts
index 92173fe..4a8dd51 100644
--- a/ReactUI/src/scripts/api/TournamentSystemAPI.ts
+++ b/ReactUI/src/scripts/api/TournamentSystemAPI.ts
@@ -14,6 +14,78 @@ export default class TournamentSystemAPI {
this.tournamentSystem = tournamentSystem;
}
+ async setTournamentName(name: string) {
+ const url = "/v1/system/settings/tournament_name";
+ const result = await this.authenticatedRequest(RequestType.POST, url, name);
+ if (result.status == 200) {
+ return {
+ success: true,
+ data: result.response
+ }
+ }
+ return this.defaultResponses(result);
+ }
+
+ async setScoreboardURL(url: string) {
+ const apiUrl = "/v1/system/settings/scoreboard_url";
+ const result = await this.authenticatedRequest(RequestType.POST, apiUrl, url);
+ if (result.status == 200) {
+ return {
+ success: true,
+ data: result.response
+ }
+ }
+ return this.defaultResponses(result);
+ }
+
+ async setMOTD(motd: string) {
+ const url = "/v1/system/settings/motd";
+ const result = await this.authenticatedRequest(RequestType.POST, url, motd);
+ if (result.status == 200) {
+ return {
+ success: true,
+ data: result.response
+ }
+ }
+ return this.defaultResponses(result);
+ }
+
+ async reloadDynamicConfig(): Promise {
+ const url = "/v1/system/dynamicconfig/reload"
+ const result = await this.authenticatedRequest(RequestType.POST, url);
+ if (result.status == 200) {
+ return {
+ success: true,
+ data: result.response
+ }
+ }
+ return this.defaultResponses(result);
+ }
+
+ async importScoreSnapshot(snapshot: any): Promise {
+ const url = "/v1/snapshot/import"
+ const result = await this.authenticatedRequest(RequestType.POST, url, snapshot);
+ if (result.status == 200) {
+ return {
+ success: true,
+ data: result.response
+ }
+ }
+ return this.defaultResponses(result);
+ }
+
+ async exportScoreSnapshot(): Promise {
+ const url = "/v1/snapshot/export"
+ const result = await this.authenticatedRequest(RequestType.GET, url);
+ if (result.status == 200) {
+ return {
+ success: true,
+ data: result.response
+ }
+ }
+ return this.defaultResponses(result);
+ }
+
async upploadTeam(teamData: TeamEditorEntry[]): Promise {
const url = "/v1/team/upload_team";
const result = await this.authenticatedRequest(RequestType.POST, url, teamData);