notif, codes, clipboard

This commit is contained in:
rdavidek 2025-12-28 20:49:44 +01:00
parent 9c59bd0d9d
commit 93c86ec580
2 changed files with 123 additions and 26 deletions

View File

@ -16,7 +16,7 @@ import java.util.stream.Collectors;
public class App {
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
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<DartsGame> convertedGames = convertRawToGames(rawGames);
GAMES_CACHE.clear();
GAMES_CACHE.addAll(convertedGames);
String code = generateCode();
GAMES_CACHE.put(code, convertedGames);
String response = "Upload successful";
byte[] responseBytes = response.getBytes(StandardCharsets.UTF_8);
exchange.sendResponseHeaders(200, responseBytes.length);
try (OutputStream os = exchange.getResponseBody()) {
os.write(responseBytes);
}
exchange.getResponseHeaders().set("Location", "/?code=" + code);
exchange.sendResponseHeaders(303, -1);
} catch (Exception e) {
e.printStackTrace();
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) {
List<DartsGame> result = new ArrayList<>();
for (RawGame rg : rawGames) {
@ -183,7 +192,15 @@ public class App {
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);
byte[] bytes = json.getBytes(StandardCharsets.UTF_8);
@ -195,6 +212,18 @@ public class App {
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) {
Map<String, List<DartsGame>> byPlayer = games.stream()
.collect(Collectors.groupingBy(DartsGame::getPlayer));

View File

@ -59,6 +59,29 @@
}
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 {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
@ -164,11 +187,21 @@
<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>
<div style="display: flex; align-items: center; gap: 10px;">
<input type="file" id="fileInput" accept=".json">
<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>
<span id="uploadStatus" style="font-weight: 600"></span>
</div>
<div id="player-summaries" class="stats-grid"></div>
@ -201,12 +234,24 @@
</div>
</div>
<div id="notification"></div>
<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() {
const fileInput = document.getElementById('fileInput');
const status = document.getElementById('uploadStatus');
if (!fileInput.files[0]) {
status.innerText = 'Please select a file';
showNotification('Please select a file', true);
return;
}
@ -216,16 +261,22 @@
try {
const response = await fetch('/api/upload', {
method: 'POST',
body: e.target.result
body: e.target.result,
redirect: 'follow'
});
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();
}
} else {
status.innerText = 'Upload failed: ' + await response.text();
showNotification('Upload failed: ' + await response.text(), true);
}
} catch (err) {
status.innerText = 'Error: ' + err.message;
showNotification('Error: ' + err.message, true);
}
};
reader.readAsText(file);
@ -233,22 +284,39 @@
async function clearCache() {
if (!confirm('Are you sure you want to clear all data?')) return;
const status = document.getElementById('uploadStatus');
try {
const response = await fetch('/api/clear', { method: 'POST' });
if (response.ok) {
status.innerText = 'Cache cleared!';
showNotification('Cache cleared!');
loadStats();
} else {
status.innerText = 'Clear failed: ' + await response.text();
showNotification('Clear failed: ' + await response.text(), true);
}
} 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() {
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();
renderSummaries(data.playerStats);