upload strava zip

This commit is contained in:
Davide Scaini
2026-04-10 22:01:44 +02:00
parent e5eadc69f2
commit 3b8bc159c5
8 changed files with 313 additions and 3 deletions
+105
View File
@@ -248,6 +248,16 @@ try {
<p id="strava-choose-sub" class="text-xs text-zinc-500">Checking…</p>
</div>
</button>
<button
id="upload-choose-zip"
class="flex items-center gap-3 p-4 rounded-lg border border-zinc-700 hover:border-zinc-500 hover:bg-zinc-800 transition-colors text-left"
>
<span class="text-2xl">📦</span>
<div>
<p class="text-sm font-medium text-white">Strava export ZIP</p>
<p class="text-xs text-zinc-500">Import your full Strava archive</p>
</div>
</button>
</div>
</div>
@@ -307,6 +317,22 @@ try {
</div>
<p id="strava-status" class="mt-3 text-xs text-center" style="min-height: 1.25rem"></p>
</div>
<!-- View: Strava ZIP upload -->
<div id="upload-view-zip" style="display:none">
<button id="upload-back-zip" class="text-xs text-zinc-500 hover:text-white mb-3 transition-colors">← Back</button>
<div class="rounded-lg border border-amber-800/50 bg-amber-950/30 p-3 mb-4 text-xs text-amber-300 leading-relaxed">
⚠ The ZIP will be processed and <strong>immediately deleted</strong> from the server — originals are not kept. Make sure you keep your own copy.
</div>
<div
id="zip-drop"
class="border-2 border-dashed border-zinc-700 rounded-lg p-6 text-center text-zinc-500 text-sm cursor-pointer hover:border-zinc-500 hover:text-zinc-300 transition-colors"
>
<div id="zip-label">Drop your Strava export .zip<br/>or click to browse</div>
<input id="zip-input" type="file" accept=".zip" class="hidden" />
</div>
<p id="zip-status" class="mt-3 text-xs text-center leading-relaxed" style="min-height: 1.25rem"></p>
</div>
</div>
</div>
)}
@@ -403,10 +429,17 @@ try {
const viewChoose = document.getElementById('upload-view-choose');
const viewFile = document.getElementById('upload-view-file');
const viewStrava = document.getElementById('upload-view-strava');
const viewZip = document.getElementById('upload-view-zip');
const chooseFile = document.getElementById('upload-choose-file');
const chooseStrava = document.getElementById('upload-choose-strava');
const chooseZip = document.getElementById('upload-choose-zip');
const backFile = document.getElementById('upload-back-file');
const backStrava = document.getElementById('upload-back-strava');
const backZip = document.getElementById('upload-back-zip');
const zipDrop = document.getElementById('zip-drop');
const zipInput = document.getElementById('zip-input');
const zipLabel = document.getElementById('zip-label');
const zipStatus = document.getElementById('zip-status');
const drop = document.getElementById('upload-drop');
const input = document.getElementById('upload-input');
const label = document.getElementById('upload-label');
@@ -427,6 +460,7 @@ try {
viewChoose.style.display = name === 'choose' ? '' : 'none';
viewFile.style.display = name === 'file' ? '' : 'none';
viewStrava.style.display = name === 'strava' ? '' : 'none';
viewZip.style.display = name === 'zip' ? '' : 'none';
}
function openModal() {
@@ -446,8 +480,10 @@ try {
document.addEventListener('keydown', e => { if (e.key === 'Escape' && modal.style.display !== 'none') closeModal(); });
chooseFile.addEventListener('click', () => showView('file'));
chooseZip.addEventListener('click', () => showView('zip'));
backFile.addEventListener('click', () => showView('choose'));
backStrava.addEventListener('click', () => showView('choose'));
backZip.addEventListener('click', () => showView('choose'));
// ── file upload ───────────────────────────────────────────────────────
drop.addEventListener('click', () => input.click());
@@ -638,6 +674,75 @@ try {
stravaResetSoftBtn.addEventListener('click', () => stravaReset('soft'));
stravaResetHardBtn.addEventListener('click', () => stravaReset('hard'));
// ── Strava ZIP upload ─────────────────────────────────────────────────
function doZipUpload(file) {
if (!file) return;
zipLabel.textContent = file.name;
zipStatus.textContent = 'Uploading…';
zipStatus.style.color = '';
const fd = new FormData();
fd.append('file', file);
// POST the file; server responds with SSE stream immediately after receiving body
const xhr = new XMLHttpRequest();
xhr.open('POST', `${editUrl}/api/upload/strava-zip`);
xhr.withCredentials = true;
xhr.setRequestHeader('Accept', 'text/event-stream');
let buf = '';
let imported = 0;
xhr.onprogress = () => {
// Parse SSE lines from the incrementally received response text
const newText = xhr.responseText.slice(buf.length);
buf = xhr.responseText;
for (const line of newText.split('\n')) {
if (!line.startsWith('data: ')) continue;
try {
const ev = JSON.parse(line.slice(6));
if (ev.type === 'validating') {
zipStatus.textContent = 'Validating ZIP structure…';
} else if (ev.type === 'extracting_csv') {
zipStatus.textContent = 'Reading activities.csv…';
} else if (ev.type === 'progress') {
const pct = Math.round((ev.n / ev.total) * 100);
const icon = ev.status === 'imported' ? '↓' : ev.status === 'error' ? '✗' : '·';
zipStatus.textContent = `${icon} ${ev.n}/${ev.total} (${pct}%) — ${ev.name}`;
if (ev.status === 'imported') imported++;
} else if (ev.type === 'done') {
const errNote = ev.error_count ? `, ${ev.error_count} errors` : '';
zipStatus.textContent = `Done — ${ev.imported} imported, ${ev.skipped} already up to date${errNote}.`;
zipStatus.style.color = '#4ade80';
zipInput.value = '';
if (ev.imported > 0) setTimeout(() => window.location.reload(), 1500);
} else if (ev.type === 'error') {
zipStatus.textContent = 'Error: ' + ev.message;
zipStatus.style.color = '#f87171';
zipInput.value = '';
}
} catch (_) {}
}
};
xhr.onerror = () => {
zipStatus.textContent = 'Upload failed — check your connection.';
zipStatus.style.color = '#f87171';
};
xhr.send(fd);
}
zipDrop.addEventListener('click', () => zipInput.click());
zipInput.addEventListener('change', () => doZipUpload(zipInput.files?.[0]));
zipDrop.addEventListener('dragover', e => { e.preventDefault(); zipDrop.classList.add('border-zinc-400'); });
zipDrop.addEventListener('dragleave', () => zipDrop.classList.remove('border-zinc-400'));
zipDrop.addEventListener('drop', e => {
e.preventDefault();
zipDrop.classList.remove('border-zinc-400');
doZipUpload(e.dataTransfer?.files?.[0]);
});
// Handle ?strava= param set by the callback redirect (popup scenario)
const sp = new URLSearchParams(window.location.search);
if (sp.has('strava')) {