initial commit
This commit is contained in:
commit
a50c03bbe6
8
.gitignore
vendored
Normal file
8
.gitignore
vendored
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
servers
|
||||||
|
target
|
||||||
|
bin
|
||||||
|
.settings
|
||||||
|
.metadata
|
||||||
|
.classpath
|
||||||
|
.project
|
||||||
|
.vscode
|
||||||
50
pom.xml
Normal file
50
pom.xml
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||||
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
|
||||||
|
<groupId>com.darts.stats</groupId>
|
||||||
|
<artifactId>darts-stats-web</artifactId>
|
||||||
|
<version>1.0-SNAPSHOT</version>
|
||||||
|
|
||||||
|
<properties>
|
||||||
|
<maven.compiler.source>11</maven.compiler.source>
|
||||||
|
<maven.compiler.target>11</maven.compiler.target>
|
||||||
|
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||||
|
</properties>
|
||||||
|
|
||||||
|
<dependencies>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.google.code.gson</groupId>
|
||||||
|
<artifactId>gson</artifactId>
|
||||||
|
<version>2.10.1</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.projectlombok</groupId>
|
||||||
|
<artifactId>lombok</artifactId>
|
||||||
|
<version>1.18.28</version>
|
||||||
|
<scope>provided</scope>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
|
||||||
|
<build>
|
||||||
|
<plugins>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
|
<artifactId>maven-compiler-plugin</artifactId>
|
||||||
|
<version>3.8.1</version>
|
||||||
|
<configuration>
|
||||||
|
<source>11</source>
|
||||||
|
<target>11</target>
|
||||||
|
<annotationProcessorPaths>
|
||||||
|
<path>
|
||||||
|
<groupId>org.projectlombok</groupId>
|
||||||
|
<artifactId>lombok</artifactId>
|
||||||
|
<version>1.18.28</version>
|
||||||
|
</path>
|
||||||
|
</annotationProcessorPaths>
|
||||||
|
</configuration>
|
||||||
|
</plugin>
|
||||||
|
</plugins>
|
||||||
|
</build>
|
||||||
|
</project>
|
||||||
210
src/main/java/com/darts/stats/App.java
Normal file
210
src/main/java/com/darts/stats/App.java
Normal file
@ -0,0 +1,210 @@
|
|||||||
|
package com.darts.stats;
|
||||||
|
|
||||||
|
import com.google.gson.Gson;
|
||||||
|
import com.google.gson.reflect.TypeToken;
|
||||||
|
import com.sun.net.httpserver.HttpExchange;
|
||||||
|
import com.sun.net.httpserver.HttpHandler;
|
||||||
|
import com.sun.net.httpserver.HttpServer;
|
||||||
|
|
||||||
|
import java.io.*;
|
||||||
|
import java.net.InetSocketAddress;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Paths;
|
||||||
|
import java.util.*;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
public class App {
|
||||||
|
private static final Gson GSON = new Gson();
|
||||||
|
private static final String DATA_FILE = "stats.json";
|
||||||
|
|
||||||
|
public static void main(String[] args) throws IOException {
|
||||||
|
int port = 8080;
|
||||||
|
HttpServer server = HttpServer.create(new InetSocketAddress(port), 0);
|
||||||
|
|
||||||
|
server.createContext("/", new StaticHandler());
|
||||||
|
server.createContext("/api/stats", new StatsHandler());
|
||||||
|
server.createContext("/api/upload", new UploadHandler());
|
||||||
|
|
||||||
|
server.setExecutor(null);
|
||||||
|
System.out.println("Server started on port " + port);
|
||||||
|
server.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
static class StaticHandler implements HttpHandler {
|
||||||
|
@Override
|
||||||
|
public void handle(HttpExchange exchange) throws IOException {
|
||||||
|
String path = exchange.getRequestURI().getPath();
|
||||||
|
if (path.equals("/")) path = "/index.html";
|
||||||
|
|
||||||
|
InputStream is = App.class.getResourceAsStream("/web" + path);
|
||||||
|
if (is == null) {
|
||||||
|
String response = "404 Not Found";
|
||||||
|
exchange.sendResponseHeaders(404, response.length());
|
||||||
|
OutputStream os = exchange.getResponseBody();
|
||||||
|
os.write(response.getBytes());
|
||||||
|
os.close();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
byte[] content = is.readAllBytes();
|
||||||
|
String contentType = "text/html";
|
||||||
|
if (path.endsWith(".js")) contentType = "application/javascript";
|
||||||
|
if (path.endsWith(".css")) contentType = "text/css";
|
||||||
|
|
||||||
|
exchange.getResponseHeaders().set("Content-Type", contentType);
|
||||||
|
exchange.sendResponseHeaders(200, content.length);
|
||||||
|
OutputStream os = exchange.getResponseBody();
|
||||||
|
os.write(content);
|
||||||
|
os.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static class UploadHandler implements HttpHandler {
|
||||||
|
@Override
|
||||||
|
public void handle(HttpExchange exchange) throws IOException {
|
||||||
|
if (!"POST".equals(exchange.getRequestMethod())) {
|
||||||
|
exchange.sendResponseHeaders(405, -1);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
InputStream is = exchange.getRequestBody();
|
||||||
|
String body = new String(is.readAllBytes(), StandardCharsets.UTF_8);
|
||||||
|
|
||||||
|
try {
|
||||||
|
List<RawGame> rawGames = GSON.fromJson(body, new TypeToken<List<RawGame>>(){}.getType());
|
||||||
|
List<DartsGame> convertedGames = convertRawToGames(rawGames);
|
||||||
|
|
||||||
|
Files.writeString(Paths.get(DATA_FILE), GSON.toJson(convertedGames));
|
||||||
|
|
||||||
|
String response = "Upload successful";
|
||||||
|
exchange.sendResponseHeaders(200, response.length());
|
||||||
|
exchange.getResponseBody().write(response.getBytes());
|
||||||
|
} catch (Exception e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
String response = "Error processing upload: " + e.getMessage();
|
||||||
|
exchange.sendResponseHeaders(400, response.length());
|
||||||
|
exchange.getResponseBody().write(response.getBytes());
|
||||||
|
} finally {
|
||||||
|
exchange.getResponseBody().close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<DartsGame> convertRawToGames(List<RawGame> rawGames) {
|
||||||
|
List<DartsGame> result = new ArrayList<>();
|
||||||
|
for (RawGame rg : rawGames) {
|
||||||
|
for (RawGame.RawPlayer rp : rg.getPlayers()) {
|
||||||
|
DartsGame game = new DartsGame();
|
||||||
|
game.setDate(rg.getDate());
|
||||||
|
game.setPlayer(rp.getName());
|
||||||
|
game.setOpponent(rg.getPlayers().stream()
|
||||||
|
.filter(p -> !p.getName().equals(rp.getName()))
|
||||||
|
.map(RawGame.RawPlayer::getName)
|
||||||
|
.collect(Collectors.joining(", ")));
|
||||||
|
game.setGameType(String.valueOf(rg.getTargetScore()));
|
||||||
|
game.setResult(rp.getName().equals(rg.getWinnerName()) ? "WIN" : "LOSS");
|
||||||
|
|
||||||
|
List<RawGame.RawThrow> throwsList = rp.getThrowsList();
|
||||||
|
game.setDartsThrown(throwsList.size());
|
||||||
|
|
||||||
|
int totalScore = throwsList.stream().filter(t -> !t.isBust()).mapToInt(RawGame.RawThrow::getValue).sum();
|
||||||
|
game.setAverage(throwsList.isEmpty() ? 0 : Math.round((double) totalScore / throwsList.size() * 3 * 100.0) / 100.0);
|
||||||
|
|
||||||
|
// Calculate visits
|
||||||
|
int h100 = 0, h140 = 0, h180 = 0, highest = 0;
|
||||||
|
for (int i = 0; i < throwsList.size(); i += 3) {
|
||||||
|
int visitSum = 0;
|
||||||
|
boolean bust = false;
|
||||||
|
for (int j = 0; j < 3 && (i + j) < throwsList.size(); j++) {
|
||||||
|
if (throwsList.get(i + j).isBust()) bust = true;
|
||||||
|
visitSum += throwsList.get(i + j).getValue();
|
||||||
|
}
|
||||||
|
if (bust) visitSum = 0;
|
||||||
|
|
||||||
|
if (visitSum >= 180) h180++;
|
||||||
|
else if (visitSum >= 140) h140++;
|
||||||
|
else if (visitSum >= 100) h100++;
|
||||||
|
|
||||||
|
if (visitSum > highest) highest = visitSum;
|
||||||
|
}
|
||||||
|
|
||||||
|
game.setHundredPlus(h100);
|
||||||
|
game.setHundredFortyPlus(h140);
|
||||||
|
game.setHundredEighty(h180);
|
||||||
|
game.setHighestVisit(highest);
|
||||||
|
|
||||||
|
result.add(game);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static class StatsHandler implements HttpHandler {
|
||||||
|
@Override
|
||||||
|
public void handle(HttpExchange exchange) throws IOException {
|
||||||
|
if (!"GET".equals(exchange.getRequestMethod())) {
|
||||||
|
exchange.sendResponseHeaders(405, -1);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<DartsGame> games = loadGames();
|
||||||
|
StatsResponse response = aggregateStats(games);
|
||||||
|
|
||||||
|
String json = GSON.toJson(response);
|
||||||
|
byte[] bytes = json.getBytes(StandardCharsets.UTF_8);
|
||||||
|
|
||||||
|
exchange.getResponseHeaders().set("Content-Type", "application/json");
|
||||||
|
exchange.sendResponseHeaders(200, bytes.length);
|
||||||
|
OutputStream os = exchange.getResponseBody();
|
||||||
|
os.write(bytes);
|
||||||
|
os.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<DartsGame> loadGames() {
|
||||||
|
try {
|
||||||
|
if (!Files.exists(Paths.get(DATA_FILE))) {
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
String content = Files.readString(Paths.get(DATA_FILE));
|
||||||
|
return GSON.fromJson(content, new TypeToken<List<DartsGame>>(){}.getType());
|
||||||
|
} catch (IOException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private StatsResponse aggregateStats(List<DartsGame> games) {
|
||||||
|
Map<String, List<DartsGame>> byPlayer = games.stream()
|
||||||
|
.collect(Collectors.groupingBy(DartsGame::getPlayer));
|
||||||
|
|
||||||
|
Map<String, StatsResponse.PlayerStats> playerStatsMap = new HashMap<>();
|
||||||
|
|
||||||
|
for (Map.Entry<String, List<DartsGame>> entry : byPlayer.entrySet()) {
|
||||||
|
String player = entry.getKey();
|
||||||
|
List<DartsGame> playerGames = entry.getValue();
|
||||||
|
|
||||||
|
double avg = playerGames.stream().mapToDouble(DartsGame::getAverage).average().orElse(0.0);
|
||||||
|
int wins = (int) playerGames.stream().filter(g -> "WIN".equalsIgnoreCase(g.getResult())).count();
|
||||||
|
int h100 = playerGames.stream().mapToInt(DartsGame::getHundredPlus).sum();
|
||||||
|
int h140 = playerGames.stream().mapToInt(DartsGame::getHundredFortyPlus).sum();
|
||||||
|
int h180 = playerGames.stream().mapToInt(DartsGame::getHundredEighty).sum();
|
||||||
|
|
||||||
|
playerStatsMap.put(player, StatsResponse.PlayerStats.builder()
|
||||||
|
.overallAverage(Math.round(avg * 100.0) / 100.0)
|
||||||
|
.totalGames(playerGames.size())
|
||||||
|
.wins(wins)
|
||||||
|
.winRate(Math.round((double) wins / playerGames.size() * 10000.0) / 100.0)
|
||||||
|
.total100plus(h100)
|
||||||
|
.total140plus(h140)
|
||||||
|
.total180s(h180)
|
||||||
|
.build());
|
||||||
|
}
|
||||||
|
|
||||||
|
return StatsResponse.builder()
|
||||||
|
.games(games)
|
||||||
|
.playerStats(playerStatsMap)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
23
src/main/java/com/darts/stats/DartsGame.java
Normal file
23
src/main/java/com/darts/stats/DartsGame.java
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
package com.darts.stats;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class DartsGame {
|
||||||
|
private String date;
|
||||||
|
private String player;
|
||||||
|
private String opponent;
|
||||||
|
private String gameType;
|
||||||
|
private String result;
|
||||||
|
private int dartsThrown;
|
||||||
|
private double average;
|
||||||
|
private int checkout;
|
||||||
|
private int highestVisit;
|
||||||
|
private int hundredPlus;
|
||||||
|
private int hundredFortyPlus;
|
||||||
|
private int hundredEighty;
|
||||||
|
}
|
||||||
25
src/main/java/com/darts/stats/RawGame.java
Normal file
25
src/main/java/com/darts/stats/RawGame.java
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
package com.darts.stats;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class RawGame {
|
||||||
|
private String date;
|
||||||
|
private List<RawPlayer> players;
|
||||||
|
private String winnerName;
|
||||||
|
private int targetScore;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public static class RawPlayer {
|
||||||
|
private String name;
|
||||||
|
private List<RawThrow> throwsList;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public static class RawThrow {
|
||||||
|
private int value;
|
||||||
|
private boolean isBust;
|
||||||
|
private String label;
|
||||||
|
}
|
||||||
|
}
|
||||||
25
src/main/java/com/darts/stats/StatsResponse.java
Normal file
25
src/main/java/com/darts/stats/StatsResponse.java
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
package com.darts.stats;
|
||||||
|
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@Builder
|
||||||
|
public class StatsResponse {
|
||||||
|
private List<DartsGame> games;
|
||||||
|
private Map<String, PlayerStats> playerStats;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@Builder
|
||||||
|
public static class PlayerStats {
|
||||||
|
private double overallAverage;
|
||||||
|
private int totalGames;
|
||||||
|
private int wins;
|
||||||
|
private int total100plus;
|
||||||
|
private int total140plus;
|
||||||
|
private int total180s;
|
||||||
|
private double winRate;
|
||||||
|
}
|
||||||
|
}
|
||||||
319
src/main/resources/web/index.html
Normal file
319
src/main/resources/web/index.html
Normal file
@ -0,0 +1,319 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Darts Stats Dashboard</title>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||||||
|
<style>
|
||||||
|
:root {
|
||||||
|
--primary: #2563eb;
|
||||||
|
--bg: #f8fafc;
|
||||||
|
--card-bg: #ffffff;
|
||||||
|
--text-main: #1e293b;
|
||||||
|
--text-muted: #64748b;
|
||||||
|
--border: #e2e8f0;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
font-family: 'Inter', system-ui, -apple-system, sans-serif;
|
||||||
|
margin: 0;
|
||||||
|
padding: 20px;
|
||||||
|
background-color: var(--bg);
|
||||||
|
color: var(--text-main);
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
.container { max-width: 1200px; margin: auto; }
|
||||||
|
h1 { font-size: 2rem; font-weight: 800; margin-bottom: 2rem; display: flex; align-items: center; gap: 10px; }
|
||||||
|
|
||||||
|
.card {
|
||||||
|
background: var(--card-bg);
|
||||||
|
padding: 24px;
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
|
||||||
|
margin-bottom: 24px;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
h2 { font-size: 1.25rem; font-weight: 700; margin-top: 0; margin-bottom: 1.5rem; color: var(--text-main); }
|
||||||
|
|
||||||
|
.upload-section {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
background: #eff6ff;
|
||||||
|
border: 1px dashed var(--primary);
|
||||||
|
}
|
||||||
|
input[type="file"] {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
button {
|
||||||
|
background: var(--primary);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
padding: 8px 16px;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: opacity 0.2s;
|
||||||
|
}
|
||||||
|
button:hover { opacity: 0.9; }
|
||||||
|
|
||||||
|
.stats-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||||
|
gap: 20px;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.player-card {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
.player-header {
|
||||||
|
font-size: 1.25rem;
|
||||||
|
font-weight: 700;
|
||||||
|
border-bottom: 2px solid var(--bg);
|
||||||
|
padding-bottom: 8px;
|
||||||
|
color: var(--primary);
|
||||||
|
}
|
||||||
|
.stat-row {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 8px 0;
|
||||||
|
}
|
||||||
|
.stat-label { color: var(--text-muted); font-size: 0.9rem; font-weight: 500; }
|
||||||
|
.stat-value { font-size: 1.1rem; font-weight: 700; color: var(--text-main); }
|
||||||
|
|
||||||
|
table { width: 100%; border-collapse: collapse; margin-top: 10px; font-size: 0.95rem; }
|
||||||
|
th {
|
||||||
|
background-color: var(--bg);
|
||||||
|
padding: 12px;
|
||||||
|
text-align: left;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-muted);
|
||||||
|
cursor: pointer;
|
||||||
|
user-select: none;
|
||||||
|
transition: background 0.2s;
|
||||||
|
}
|
||||||
|
th:hover { background-color: var(--border); }
|
||||||
|
td { padding: 12px; border-bottom: 1px solid var(--border); }
|
||||||
|
tr:last-child td { border-bottom: none; }
|
||||||
|
|
||||||
|
.badge {
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
font-weight: 700;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
.badge-win { background: #dcfce7; color: #166534; }
|
||||||
|
.badge-loss { background: #fee2e2; color: #991b1b; }
|
||||||
|
|
||||||
|
.chart-container { position: relative; height: 350px; width: 100%; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<h1>🎯 Darts Statistics Dashboard</h1>
|
||||||
|
|
||||||
|
<div class="card upload-section">
|
||||||
|
<div style="flex-grow: 1">
|
||||||
|
<h2 style="margin: 0 0 8px 0; font-size: 1.1rem">Upload Stats JSON</h2>
|
||||||
|
<input type="file" id="fileInput" accept=".json">
|
||||||
|
<button onclick="uploadFile()">Upload</button>
|
||||||
|
</div>
|
||||||
|
<span id="uploadStatus" style="font-weight: 600"></span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="player-summaries" class="stats-grid"></div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h2>Average Progression</h2>
|
||||||
|
<div class="chart-container">
|
||||||
|
<canvas id="avgChart"></canvas>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h2>Recent Games</h2>
|
||||||
|
<table id="games-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th onclick="sortTable(0)">Date ↕</th>
|
||||||
|
<th onclick="sortTable(1)">Player ↕</th>
|
||||||
|
<th onclick="sortTable(2)">Opponent ↕</th>
|
||||||
|
<th onclick="sortTable(3)">Avg ↕</th>
|
||||||
|
<th onclick="sortTable(4)">Darts ↕</th>
|
||||||
|
<th onclick="sortTable(5)">Round ↕</th>
|
||||||
|
<th onclick="sortTable(6)">Result ↕</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody></tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
async function uploadFile() {
|
||||||
|
const fileInput = document.getElementById('fileInput');
|
||||||
|
const status = document.getElementById('uploadStatus');
|
||||||
|
if (!fileInput.files[0]) {
|
||||||
|
status.innerText = 'Please select a file';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const file = fileInput.files[0];
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = async (e) => {
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/upload', {
|
||||||
|
method: 'POST',
|
||||||
|
body: e.target.result
|
||||||
|
});
|
||||||
|
if (response.ok) {
|
||||||
|
status.innerText = 'Upload successful!';
|
||||||
|
loadStats();
|
||||||
|
} else {
|
||||||
|
status.innerText = 'Upload failed: ' + await response.text();
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
status.innerText = 'Error: ' + err.message;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
reader.readAsText(file);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadStats() {
|
||||||
|
const response = await fetch('/api/stats');
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
renderSummaries(data.playerStats);
|
||||||
|
renderChart(data.games);
|
||||||
|
renderTable(data.games);
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderSummaries(playerStats) {
|
||||||
|
const container = document.getElementById('player-summaries');
|
||||||
|
container.innerHTML = '';
|
||||||
|
|
||||||
|
for (const [player, stats] of Object.entries(playerStats)) {
|
||||||
|
const card = document.createElement('div');
|
||||||
|
card.className = 'card player-card';
|
||||||
|
card.innerHTML = `
|
||||||
|
<div class="player-header">${player}</div>
|
||||||
|
<div class="stat-row">
|
||||||
|
<span class="stat-label">Overall Average</span>
|
||||||
|
<span class="stat-value">${stats.overallAverage}</span>
|
||||||
|
</div>
|
||||||
|
<div class="stat-row">
|
||||||
|
<span class="stat-label">Win Rate</span>
|
||||||
|
<span class="stat-value">${stats.winRate}%</span>
|
||||||
|
</div>
|
||||||
|
<div class="stat-row">
|
||||||
|
<span class="stat-label">Total 180s</span>
|
||||||
|
<span class="stat-value">${stats.total180s}</span>
|
||||||
|
</div>
|
||||||
|
<div class="stat-row">
|
||||||
|
<span class="stat-label">Games Played</span>
|
||||||
|
<span class="stat-value">${stats.totalGames}</span>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
container.appendChild(card);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderChart(games) {
|
||||||
|
const ctx = document.getElementById('avgChart').getContext('2d');
|
||||||
|
if (window.myChart) window.myChart.destroy();
|
||||||
|
|
||||||
|
const players = [...new Set(games.map(g => g.player))];
|
||||||
|
const dates = [...new Set(games.map(g => g.date))].sort((a, b) => new Date(a) - new Date(b));
|
||||||
|
|
||||||
|
const datasets = players.map(player => {
|
||||||
|
const playerGames = games.filter(g => g.player === player);
|
||||||
|
const data = dates.map(date => {
|
||||||
|
const game = playerGames.find(g => g.date === date);
|
||||||
|
return game ? game.average : null;
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
label: player,
|
||||||
|
data: data,
|
||||||
|
borderColor: '#' + Math.floor(Math.random()*16777215).toString(16),
|
||||||
|
fill: false,
|
||||||
|
tension: 0.1,
|
||||||
|
spanGaps: true
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
window.myChart = new Chart(ctx, {
|
||||||
|
type: 'line',
|
||||||
|
data: {
|
||||||
|
labels: dates,
|
||||||
|
datasets: datasets
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
responsive: true,
|
||||||
|
maintainAspectRatio: false,
|
||||||
|
scales: {
|
||||||
|
y: { beginAtZero: false, title: { display: true, text: '3-Dart Average' } }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderTable(games) {
|
||||||
|
const tbody = document.querySelector('#games-table tbody');
|
||||||
|
tbody.innerHTML = '';
|
||||||
|
|
||||||
|
const sortedGames = games.slice().sort((a, b) => new Date(b.date) - new Date(a.date));
|
||||||
|
|
||||||
|
sortedGames.forEach(game => {
|
||||||
|
const tr = document.createElement('tr');
|
||||||
|
const resultClass = game.result === 'WIN' ? 'badge-win' : 'badge-loss';
|
||||||
|
const round = Math.ceil(game.dartsThrown / 3);
|
||||||
|
tr.innerHTML = `
|
||||||
|
<td style="font-weight: 500">${game.date}</td>
|
||||||
|
<td style="font-weight: 600">${game.player}</td>
|
||||||
|
<td>${game.opponent}</td>
|
||||||
|
<td style="font-weight: 700; color: var(--primary)">${game.average}</td>
|
||||||
|
<td>${game.dartsThrown}</td>
|
||||||
|
<td>${round}</td>
|
||||||
|
<td><span class="badge ${resultClass}">${game.result}</span></td>
|
||||||
|
`;
|
||||||
|
tbody.appendChild(tr);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function sortTable(n) {
|
||||||
|
const table = document.getElementById("games-table");
|
||||||
|
const tbody = table.querySelector("tbody");
|
||||||
|
const rows = Array.from(tbody.rows);
|
||||||
|
|
||||||
|
let dir = table.getAttribute("data-sort-dir") === "asc" && table.getAttribute("data-sort-col") == n ? "desc" : "asc";
|
||||||
|
|
||||||
|
rows.sort((a, b) => {
|
||||||
|
let x = a.cells[n].textContent.trim().toLowerCase();
|
||||||
|
let y = b.cells[n].textContent.trim().toLowerCase();
|
||||||
|
|
||||||
|
if (n === 3 || n === 4 || n === 5) { // Numeric (Avg, Darts, Round)
|
||||||
|
return dir === "asc" ? parseFloat(x) - parseFloat(y) : parseFloat(y) - parseFloat(x);
|
||||||
|
}
|
||||||
|
if (n === 0) { // Date
|
||||||
|
return dir === "asc" ? new Date(x) - new Date(y) : new Date(y) - new Date(x);
|
||||||
|
}
|
||||||
|
// String
|
||||||
|
return dir === "asc" ? x.localeCompare(y) : y.localeCompare(x);
|
||||||
|
});
|
||||||
|
|
||||||
|
table.setAttribute("data-sort-dir", dir);
|
||||||
|
table.setAttribute("data-sort-col", n);
|
||||||
|
|
||||||
|
rows.forEach(row => tbody.appendChild(row));
|
||||||
|
}
|
||||||
|
|
||||||
|
loadStats();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Loading…
x
Reference in New Issue
Block a user