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