upload strava zip
This commit is contained in:
@@ -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')) {
|
||||
|
||||
Reference in New Issue
Block a user