notif, codes, clipboard
This commit is contained in:
parent
9c59bd0d9d
commit
93c86ec580
@ -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));
|
||||
|
||||
@ -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);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user