First commit for bicorder-app
This commit is contained in:
233
bicorder-app/src/components/ExportControls.svelte
Normal file
233
bicorder-app/src/components/ExportControls.svelte
Normal file
@@ -0,0 +1,233 @@
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
import type { BicorderState } from '../types';
|
||||
|
||||
export let data: BicorderState;
|
||||
|
||||
const dispatch = createEventDispatcher<{
|
||||
reset: void;
|
||||
}>();
|
||||
|
||||
let showUploadDialog = false;
|
||||
let isUploading = false;
|
||||
|
||||
// Gitea configuration
|
||||
const GITEA_TOKEN = 'd495e72e955c00be2de0f1e18183f6a385b6e52c';
|
||||
const GITEA_API_URL = 'https://git.medlab.host/api/v1';
|
||||
const REPO_OWNER = 'ntnsndr';
|
||||
const REPO_NAME = 'protocol-bicorder-data';
|
||||
const REPO_URL = `https://git.medlab.host/${REPO_OWNER}/${REPO_NAME}`;
|
||||
const APP_VERSION = '1.0.0';
|
||||
|
||||
function exportToJSON() {
|
||||
const json = JSON.stringify(data, null, 2);
|
||||
const blob = new Blob([json], { type: 'application/json' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `bicorder-${data.metadata.protocol || 'diagnostic'}-${new Date().toISOString().slice(0, 10)}.json`;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
|
||||
async function shareResults() {
|
||||
const json = JSON.stringify(data, null, 2);
|
||||
const file = new File([json], `bicorder-${data.metadata.protocol || 'diagnostic'}.json`, {
|
||||
type: 'application/json',
|
||||
});
|
||||
|
||||
if (navigator.share && navigator.canShare && navigator.canShare({ files: [file] })) {
|
||||
try {
|
||||
await navigator.share({
|
||||
title: 'Protocol Bicorder Diagnostic',
|
||||
text: `Diagnostic for: ${data.metadata.protocol || 'Protocol'}`,
|
||||
files: [file],
|
||||
});
|
||||
} catch (err) {
|
||||
if ((err as Error).name !== 'AbortError') {
|
||||
console.error('Share failed:', err);
|
||||
alert('Share failed. Try using the Export button instead.');
|
||||
}
|
||||
}
|
||||
} else {
|
||||
alert('Web Share API not supported. Use the Export button to download the file.');
|
||||
}
|
||||
}
|
||||
|
||||
async function uploadReadings() {
|
||||
isUploading = true;
|
||||
|
||||
try {
|
||||
// Generate filename with timestamp
|
||||
const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, -5) + 'Z';
|
||||
const filename = `bicorder-${timestamp}.json`;
|
||||
const filepath = `readings/${filename}`;
|
||||
|
||||
// Create commit message
|
||||
const protocolName = data.metadata.protocol || 'Unknown Protocol';
|
||||
const analystName = data.metadata.analyst || 'Anonymous';
|
||||
const commitMessage = `Bicorder reading: ${protocolName} by ${analystName} | Source: Protocol Bicorder v${APP_VERSION}`;
|
||||
|
||||
// Prepare the content (base64 encoded)
|
||||
const jsonContent = JSON.stringify(data, null, 2);
|
||||
const base64Content = btoa(unescape(encodeURIComponent(jsonContent)));
|
||||
|
||||
// Upload to Gitea
|
||||
const response = await fetch(
|
||||
`${GITEA_API_URL}/repos/${REPO_OWNER}/${REPO_NAME}/contents/${filepath}`,
|
||||
{
|
||||
method: 'POST',
|
||||
mode: 'cors',
|
||||
headers: {
|
||||
'Authorization': `token ${GITEA_TOKEN}`,
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
content: base64Content,
|
||||
message: commitMessage,
|
||||
branch: 'main',
|
||||
}),
|
||||
}
|
||||
);
|
||||
|
||||
if (response.ok) {
|
||||
alert('Successfully uploaded! Your reading is now public.');
|
||||
showUploadDialog = false;
|
||||
} else {
|
||||
const errorData = await response.json();
|
||||
throw new Error(errorData.message || `Upload failed: ${response.statusContents}`);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Upload error:', err);
|
||||
alert(`Upload error: ${(err as Error).message}`);
|
||||
} finally {
|
||||
isUploading = false;
|
||||
}
|
||||
}
|
||||
|
||||
function handleReset() {
|
||||
dispatch('reset');
|
||||
}
|
||||
</script>
|
||||
|
||||
<section class="export-controls">
|
||||
<div class="button-group">
|
||||
<button on:click={exportToJSON}>
|
||||
💾 Export JSON
|
||||
</button>
|
||||
|
||||
{#if navigator.share}
|
||||
<button on:click={shareResults}>
|
||||
📤 Share
|
||||
</button>
|
||||
{/if}
|
||||
|
||||
<button on:click={() => showUploadDialog = !showUploadDialog}>
|
||||
📤 Upload
|
||||
</button>
|
||||
|
||||
<button class="reset-btn" on:click={handleReset}>
|
||||
🗑️ Reset All
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{#if showUploadDialog}
|
||||
<div class="webhook-config">
|
||||
<p class="upload-confirmation">
|
||||
Are you sure you are ready to share your readings publicly?
|
||||
</p>
|
||||
<p class="upload-terms">
|
||||
Submitted readings are posted publicly and licensed to the public domain. By proceeding, you agree to these terms.
|
||||
</p>
|
||||
<p class="upload-repo-link">
|
||||
Readings are posted to <a href={REPO_URL} target="_blank" rel="noopener noreferrer">this repository</a>.
|
||||
</p>
|
||||
<div class="webhook-buttons">
|
||||
<button on:click={uploadReadings} disabled={isUploading}>
|
||||
{isUploading ? 'Uploading...' : 'Proceed'}
|
||||
</button>
|
||||
<button on:click={() => showUploadDialog = false} disabled={isUploading}>Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</section>
|
||||
|
||||
<style>
|
||||
.export-controls {
|
||||
margin: 2rem 0;
|
||||
padding: 1rem;
|
||||
border-top: 2px solid var(--border-color);
|
||||
}
|
||||
|
||||
.button-group {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.reset-btn {
|
||||
border-color: #ff0000;
|
||||
color: #ff0000;
|
||||
}
|
||||
|
||||
.reset-btn:hover {
|
||||
background-color: #ff0000;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.webhook-config {
|
||||
margin-top: 1rem;
|
||||
padding: 1rem;
|
||||
border: 2px solid var(--border-color);
|
||||
}
|
||||
|
||||
.upload-confirmation {
|
||||
font-size: 1rem;
|
||||
font-weight: bold;
|
||||
margin-bottom: 1rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.upload-terms {
|
||||
font-size: 0.85rem;
|
||||
opacity: 0.7;
|
||||
line-height: 1.4;
|
||||
margin-bottom: 1rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.upload-repo-link {
|
||||
font-size: 0.9rem;
|
||||
margin-bottom: 1rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.upload-repo-link a {
|
||||
color: var(--fg-color);
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.upload-repo-link a:hover {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.webhook-buttons {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.webhook-buttons button {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.button-group {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user