notif, codes, clipboard
This commit is contained in:
parent
9c59bd0d9d
commit
93c86ec580
@ -16,7 +16,7 @@ import java.util.stream.Collectors;
|
|||||||
|
|
||||||
public class App {
|
public class App {
|
||||||
private static final Gson GSON = new Gson();
|
private static final Gson GSON = new Gson();
|
||||||
private static final List<DartsGame> GAMES_CACHE = Collections.synchronizedList(new ArrayList<>());
|
private static final Map<String, List<DartsGame>> GAMES_CACHE = Collections.synchronizedMap(new HashMap<>());
|
||||||
private static final long MAX_UPLOAD_SIZE = 5 * 1024 * 1024; // 5MB
|
private static final long MAX_UPLOAD_SIZE = 5 * 1024 * 1024; // 5MB
|
||||||
|
|
||||||
public static void main(String[] args) throws IOException {
|
public static void main(String[] args) throws IOException {
|
||||||
@ -104,21 +104,30 @@ public class App {
|
|||||||
List<RawGame> rawGames = GSON.fromJson(body, new TypeToken<List<RawGame>>(){}.getType());
|
List<RawGame> rawGames = GSON.fromJson(body, new TypeToken<List<RawGame>>(){}.getType());
|
||||||
List<DartsGame> convertedGames = convertRawToGames(rawGames);
|
List<DartsGame> convertedGames = convertRawToGames(rawGames);
|
||||||
|
|
||||||
GAMES_CACHE.clear();
|
String code = generateCode();
|
||||||
GAMES_CACHE.addAll(convertedGames);
|
GAMES_CACHE.put(code, convertedGames);
|
||||||
|
|
||||||
String response = "Upload successful";
|
exchange.getResponseHeaders().set("Location", "/?code=" + code);
|
||||||
byte[] responseBytes = response.getBytes(StandardCharsets.UTF_8);
|
exchange.sendResponseHeaders(303, -1);
|
||||||
exchange.sendResponseHeaders(200, responseBytes.length);
|
|
||||||
try (OutputStream os = exchange.getResponseBody()) {
|
|
||||||
os.write(responseBytes);
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
e.printStackTrace();
|
e.printStackTrace();
|
||||||
sendError(exchange, 400, "Error processing upload: " + e.getMessage());
|
sendError(exchange, 400, "Error processing upload: " + e.getMessage());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private String generateCode() {
|
||||||
|
String chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
|
||||||
|
Random rnd = new Random();
|
||||||
|
StringBuilder sb;
|
||||||
|
do {
|
||||||
|
sb = new StringBuilder(5);
|
||||||
|
for (int i = 0; i < 5; i++) {
|
||||||
|
sb.append(chars.charAt(rnd.nextInt(chars.length())));
|
||||||
|
}
|
||||||
|
} while (GAMES_CACHE.containsKey(sb.toString()));
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
|
||||||
private List<DartsGame> convertRawToGames(List<RawGame> rawGames) {
|
private List<DartsGame> convertRawToGames(List<RawGame> rawGames) {
|
||||||
List<DartsGame> result = new ArrayList<>();
|
List<DartsGame> result = new ArrayList<>();
|
||||||
for (RawGame rg : rawGames) {
|
for (RawGame rg : rawGames) {
|
||||||
@ -183,7 +192,15 @@ public class App {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
StatsResponse response = aggregateStats(new ArrayList<>(GAMES_CACHE));
|
Map<String, String> params = parseQuery(exchange.getRequestURI().getQuery());
|
||||||
|
String code = params.get("code");
|
||||||
|
|
||||||
|
List<DartsGame> games = (code != null) ? GAMES_CACHE.get(code.toUpperCase()) : null;
|
||||||
|
if (games == null) {
|
||||||
|
games = Collections.emptyList();
|
||||||
|
}
|
||||||
|
|
||||||
|
StatsResponse response = aggregateStats(new ArrayList<>(games));
|
||||||
|
|
||||||
String json = GSON.toJson(response);
|
String json = GSON.toJson(response);
|
||||||
byte[] bytes = json.getBytes(StandardCharsets.UTF_8);
|
byte[] bytes = json.getBytes(StandardCharsets.UTF_8);
|
||||||
@ -195,6 +212,18 @@ public class App {
|
|||||||
os.close();
|
os.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private Map<String, String> parseQuery(String query) {
|
||||||
|
if (query == null) return Collections.emptyMap();
|
||||||
|
Map<String, String> result = new HashMap<>();
|
||||||
|
for (String param : query.split("&")) {
|
||||||
|
String[] entry = param.split("=");
|
||||||
|
if (entry.length > 1) {
|
||||||
|
result.put(entry[0], entry[1]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
private StatsResponse aggregateStats(List<DartsGame> games) {
|
private StatsResponse aggregateStats(List<DartsGame> games) {
|
||||||
Map<String, List<DartsGame>> byPlayer = games.stream()
|
Map<String, List<DartsGame>> byPlayer = games.stream()
|
||||||
.collect(Collectors.groupingBy(DartsGame::getPlayer));
|
.collect(Collectors.groupingBy(DartsGame::getPlayer));
|
||||||
|
|||||||
@ -59,6 +59,29 @@
|
|||||||
}
|
}
|
||||||
button:hover { background: var(--primary-hover); transform: translateY(-1px); }
|
button:hover { background: var(--primary-hover); transform: translateY(-1px); }
|
||||||
|
|
||||||
|
.icon-button:hover {
|
||||||
|
color: var(--primary) !important;
|
||||||
|
transform: scale(1.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
#notification {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 20px;
|
||||||
|
right: 20px;
|
||||||
|
background: var(--primary);
|
||||||
|
color: #052e16;
|
||||||
|
padding: 12px 24px;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-weight: 700;
|
||||||
|
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.3);
|
||||||
|
transform: translateY(100px);
|
||||||
|
transition: transform 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275);
|
||||||
|
z-index: 1000;
|
||||||
|
}
|
||||||
|
#notification.show {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
|
||||||
.stats-grid {
|
.stats-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||||
@ -164,11 +187,21 @@
|
|||||||
<div class="card upload-section">
|
<div class="card upload-section">
|
||||||
<div style="flex-grow: 1">
|
<div style="flex-grow: 1">
|
||||||
<h2 style="margin: 0 0 8px 0; font-size: 1.1rem">Upload Stats JSON</h2>
|
<h2 style="margin: 0 0 8px 0; font-size: 1.1rem">Upload Stats JSON</h2>
|
||||||
|
<div style="display: flex; align-items: center; gap: 10px;">
|
||||||
<input type="file" id="fileInput" accept=".json">
|
<input type="file" id="fileInput" accept=".json">
|
||||||
<button onclick="uploadFile()">Upload</button>
|
<button onclick="uploadFile()">Upload</button>
|
||||||
<button onclick="clearCache()" style="background: #475569; margin-left: 10px;">Clear Cache</button>
|
<button onclick="clearCache()" style="background: #475569;">Clear Cache</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="currentCodeDisplay" style="background: #0f172a; padding: 10px 15px; border-radius: 8px; border: 1px solid var(--border); display: flex; flex-direction: column; align-items: center; min-width: 110px;">
|
||||||
|
<div style="font-size: 0.7rem; color: var(--text-muted); text-transform: uppercase; font-weight: 700;">Current Code</div>
|
||||||
|
<div style="display: flex; align-items: center; gap: 10px; margin-top: 4px;">
|
||||||
|
<div id="activeCode" style="font-size: 1.3rem; font-weight: 800; color: var(--primary); font-family: 'JetBrains Mono', monospace;">NONE</div>
|
||||||
|
<span onclick="copyUrlToClipboard()" class="icon-button" style="cursor: pointer; font-size: 1.2rem; line-height: 1; filter: grayscale(1) brightness(1.5);" title="Copy URL to clipboard">
|
||||||
|
📋
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<span id="uploadStatus" style="font-weight: 600"></span>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="player-summaries" class="stats-grid"></div>
|
<div id="player-summaries" class="stats-grid"></div>
|
||||||
@ -201,12 +234,24 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div id="notification"></div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
function showNotification(message, isError = false) {
|
||||||
|
const nav = document.getElementById('notification');
|
||||||
|
nav.innerText = message;
|
||||||
|
nav.style.background = isError ? '#ef4444' : 'var(--primary)';
|
||||||
|
nav.style.color = isError ? '#fff' : '#052e16';
|
||||||
|
nav.classList.add('show');
|
||||||
|
setTimeout(() => {
|
||||||
|
nav.classList.remove('show');
|
||||||
|
}, 3000);
|
||||||
|
}
|
||||||
|
|
||||||
async function uploadFile() {
|
async function uploadFile() {
|
||||||
const fileInput = document.getElementById('fileInput');
|
const fileInput = document.getElementById('fileInput');
|
||||||
const status = document.getElementById('uploadStatus');
|
|
||||||
if (!fileInput.files[0]) {
|
if (!fileInput.files[0]) {
|
||||||
status.innerText = 'Please select a file';
|
showNotification('Please select a file', true);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -216,16 +261,22 @@
|
|||||||
try {
|
try {
|
||||||
const response = await fetch('/api/upload', {
|
const response = await fetch('/api/upload', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: e.target.result
|
body: e.target.result,
|
||||||
|
redirect: 'follow'
|
||||||
});
|
});
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
status.innerText = 'Upload successful!';
|
showNotification('Upload successful!');
|
||||||
|
const url = new URL(response.url);
|
||||||
|
const code = url.searchParams.get('code');
|
||||||
|
if (code) {
|
||||||
|
window.history.pushState({}, '', `/?code=${code}`);
|
||||||
loadStats();
|
loadStats();
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
status.innerText = 'Upload failed: ' + await response.text();
|
showNotification('Upload failed: ' + await response.text(), true);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
status.innerText = 'Error: ' + err.message;
|
showNotification('Error: ' + err.message, true);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
reader.readAsText(file);
|
reader.readAsText(file);
|
||||||
@ -233,22 +284,39 @@
|
|||||||
|
|
||||||
async function clearCache() {
|
async function clearCache() {
|
||||||
if (!confirm('Are you sure you want to clear all data?')) return;
|
if (!confirm('Are you sure you want to clear all data?')) return;
|
||||||
const status = document.getElementById('uploadStatus');
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch('/api/clear', { method: 'POST' });
|
const response = await fetch('/api/clear', { method: 'POST' });
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
status.innerText = 'Cache cleared!';
|
showNotification('Cache cleared!');
|
||||||
loadStats();
|
loadStats();
|
||||||
} else {
|
} else {
|
||||||
status.innerText = 'Clear failed: ' + await response.text();
|
showNotification('Clear failed: ' + await response.text(), true);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
status.innerText = 'Error: ' + err.message;
|
showNotification('Error: ' + err.message, true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function copyUrlToClipboard() {
|
||||||
|
const code = document.getElementById('activeCode').innerText;
|
||||||
|
if (code === 'NONE') return;
|
||||||
|
|
||||||
|
const url = window.location.href;
|
||||||
|
navigator.clipboard.writeText(url).then(() => {
|
||||||
|
showNotification('URL copied to clipboard!');
|
||||||
|
}).catch(err => {
|
||||||
|
console.error('Could not copy text: ', err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async function loadStats() {
|
async function loadStats() {
|
||||||
const response = await fetch('/api/stats');
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
|
const code = urlParams.get('code');
|
||||||
|
|
||||||
|
const displayCode = document.getElementById('activeCode');
|
||||||
|
displayCode.innerText = code ? code.toUpperCase() : 'NONE';
|
||||||
|
|
||||||
|
const response = await fetch(`/api/stats${code ? `?code=${code}` : ''}`);
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
|
||||||
renderSummaries(data.playerStats);
|
renderSummaries(data.playerStats);
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user