@le-space/orbitdb-storage-bridge 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,965 @@
1
+ <script>
2
+ import { onMount } from 'svelte';
3
+ import {
4
+ Cloud,
5
+ Upload,
6
+ Plus,
7
+ List,
8
+ Key,
9
+ Mail,
10
+ LogOut,
11
+ Loader2,
12
+ AlertCircle,
13
+ CheckCircle,
14
+ Download
15
+ } from 'lucide-svelte';
16
+ import {
17
+ initializeStorachaClient,
18
+ createStorachaAccount,
19
+ listSpaces,
20
+ createSpace
21
+ } from './storacha-backup.js';
22
+ import { OrbitDBStorachaBridge, restoreDatabaseFromSpace } from 'orbitdb-storage-bridge';
23
+ import { todosStore } from './db-actions.js';
24
+ import { initializationStore, orbitDBStore, libp2pStore, peerIdStore } from './p2p.js';
25
+ import { initializeDatabase, loadTodos, todoDBStore } from './db-actions.js';
26
+ import { logger } from '../lib/logger.js';
27
+ // Add imports for creating fresh instances
28
+ import { createHelia } from 'helia';
29
+ import { createOrbitDB } from '@orbitdb/core';
30
+ import { createLibp2pConfig } from './libp2p-config.js';
31
+ import { LevelBlockstore } from 'blockstore-level';
32
+ import { LevelDatastore } from 'datastore-level';
33
+
34
+ // Component state
35
+ let showStoracha = false;
36
+ let isLoading = false;
37
+ let status = '';
38
+ let error = null;
39
+ let success = null;
40
+
41
+ // Auth state
42
+ let isLoggedIn = false;
43
+ let client = null;
44
+ let currentSpace = null;
45
+
46
+ // Progress tracking state
47
+ let showProgress = false;
48
+ let progressType = ''; // 'upload' or 'download'
49
+ let progressCurrent = 0;
50
+ let progressTotal = 0;
51
+ let progressPercentage = 0;
52
+ let progressCurrentBlock = null;
53
+ let progressError = null;
54
+
55
+ // Bridge instance
56
+ let bridge = null;
57
+
58
+ // LocalStorage keys
59
+ const STORAGE_KEYS = {
60
+ STORACHA_KEY: 'storacha_key',
61
+ STORACHA_PROOF: 'storacha_proof',
62
+ AUTO_LOGIN: 'storacha_auto_login'
63
+ };
64
+
65
+ // Form state
66
+ let showCreateForm = false;
67
+ let showCredentialsForm = false;
68
+ let email = '';
69
+ let storachaKey = '';
70
+ let storachaProof = '';
71
+ let newSpaceName = '';
72
+
73
+ // Data state
74
+ let spaces = [];
75
+
76
+
77
+
78
+ // Progress tracking functions
79
+ function initializeBridge(storachaKey, storachaProof) {
80
+ if (bridge) {
81
+ // Remove existing listeners
82
+ bridge.removeAllListeners();
83
+ }
84
+
85
+ bridge = new OrbitDBStorachaBridge({
86
+ storachaKey,
87
+ storachaProof
88
+ });
89
+
90
+ // Set up progress event listeners
91
+ bridge.on('uploadProgress', (progress) => {
92
+ logger.info({
93
+ current: progress.current,
94
+ total: progress.total,
95
+ percentage: progress.percentage
96
+ }, `Upload Progress: ${progress.current}/${progress.total} (${progress.percentage}%)`);
97
+
98
+ progressType = 'upload';
99
+ progressCurrent = progress.current;
100
+ progressTotal = progress.total;
101
+ progressPercentage = progress.percentage;
102
+ progressCurrentBlock = progress.currentBlock;
103
+ progressError = progress.error;
104
+ showProgress = true;
105
+
106
+ // Update status text
107
+ if (progress.error) {
108
+ status = `Upload error: ${progress.error.message}`;
109
+ } else if (progress.currentBlock) {
110
+ status = `Uploading block ${progress.current} of ${progress.total} (${progress.currentBlock.hash.slice(0, 8)}...)`;
111
+ } else {
112
+ status = `Uploading block ${progress.current} of ${progress.total}`;
113
+ }
114
+ });
115
+
116
+ bridge.on('downloadProgress', (progress) => {
117
+ logger.info({
118
+ current: progress.current,
119
+ total: progress.total,
120
+ percentage: progress.percentage
121
+ }, `Download Progress: ${progress.current}/${progress.total} (${progress.percentage}%)`);
122
+
123
+ progressType = 'download';
124
+ progressCurrent = progress.current;
125
+ progressTotal = progress.total;
126
+ progressPercentage = progress.percentage;
127
+ progressCurrentBlock = progress.currentBlock;
128
+ progressError = progress.error;
129
+ showProgress = true;
130
+
131
+ // Update status text
132
+ if (progress.error) {
133
+ status = `Download error: ${progress.error.message}`;
134
+ } else if (progress.currentBlock) {
135
+ status = `Downloading block ${progress.current} of ${progress.total} (${progress.currentBlock.storachaCID.slice(0, 8)}...)`;
136
+ } else {
137
+ status = `Downloading block ${progress.current} of ${progress.total}`;
138
+ }
139
+ });
140
+
141
+ return bridge;
142
+ }
143
+
144
+ function resetProgress() {
145
+ showProgress = false;
146
+ progressType = '';
147
+ progressCurrent = 0;
148
+ progressTotal = 0;
149
+ progressPercentage = 0;
150
+ progressCurrentBlock = null;
151
+ progressError = null;
152
+ }
153
+
154
+ // LocalStorage functions
155
+ function saveCredentials(key, proof) {
156
+ try {
157
+ logger.info('💾 Saving credentials to localStorage...');
158
+ localStorage.setItem(STORAGE_KEYS.STORACHA_KEY, key);
159
+ localStorage.setItem(STORAGE_KEYS.STORACHA_PROOF, proof);
160
+ localStorage.setItem(STORAGE_KEYS.AUTO_LOGIN, 'true');
161
+ logger.info('✅ Credentials saved successfully');
162
+ } catch (err) {
163
+ logger.warn({ error: err.message }, '❌ Failed to save credentials to localStorage');
164
+ }
165
+ }
166
+
167
+ function loadCredentials() {
168
+ try {
169
+ logger.info('📖 Checking localStorage for credentials...');
170
+ const key = localStorage.getItem(STORAGE_KEYS.STORACHA_KEY);
171
+ const proof = localStorage.getItem(STORAGE_KEYS.STORACHA_PROOF);
172
+ const autoLogin = localStorage.getItem(STORAGE_KEYS.AUTO_LOGIN);
173
+
174
+ if (key && proof && autoLogin === 'true') {
175
+ logger.info('✅ Valid credentials found!');
176
+ return { key, proof };
177
+ } else {
178
+ logger.info('❌ No valid credentials found');
179
+ }
180
+ } catch (err) {
181
+ logger.warn({ error: err.message }, '❌ Failed to load credentials from localStorage');
182
+ }
183
+ return null;
184
+ }
185
+
186
+ function clearStoredCredentials() {
187
+ try {
188
+ localStorage.removeItem(STORAGE_KEYS.STORACHA_KEY);
189
+ localStorage.removeItem(STORAGE_KEYS.STORACHA_PROOF);
190
+ localStorage.removeItem(STORAGE_KEYS.AUTO_LOGIN);
191
+ } catch (err) {
192
+ logger.warn({ error: err.message }, 'Failed to clear credentials from localStorage');
193
+ }
194
+ }
195
+
196
+ // Auto-hide messages
197
+ function showMessage(message, type = 'info') {
198
+ if (type === 'error') {
199
+ error = message;
200
+ success = null;
201
+ } else {
202
+ success = message;
203
+ error = null;
204
+ }
205
+
206
+ setTimeout(() => {
207
+ error = null;
208
+ success = null;
209
+ }, 5000);
210
+ }
211
+
212
+ // Clear forms
213
+ function clearForms() {
214
+ showCreateForm = false;
215
+ showCredentialsForm = false;
216
+ email = '';
217
+ storachaKey = '';
218
+ storachaProof = '';
219
+ newSpaceName = '';
220
+ }
221
+
222
+ // Login with email
223
+ async function handleEmailLogin() {
224
+ if (!email.trim()) {
225
+ showMessage('Please enter your email address', 'error');
226
+ return;
227
+ }
228
+
229
+ isLoading = true;
230
+ status = 'Creating account...';
231
+
232
+ try {
233
+ const result = await createStorachaAccount(email.trim());
234
+
235
+ if (result.success) {
236
+ showMessage(result.message);
237
+ clearForms();
238
+ } else {
239
+ showMessage(result.error, 'error');
240
+ }
241
+ } catch (err) {
242
+ showMessage(`Failed to create account: ${err.message}`, 'error');
243
+ } finally {
244
+ isLoading = false;
245
+ status = '';
246
+ }
247
+ }
248
+
249
+ // Login with credentials
250
+ async function handleCredentialsLogin(useStoredCredentials = false) {
251
+ logger.info({ useStoredCredentials }, '🚀 handleCredentialsLogin called with useStoredCredentials =');
252
+ let keyToUse = storachaKey.trim();
253
+ let proofToUse = storachaProof.trim();
254
+
255
+ logger.info({
256
+ storachaKey: storachaKey ? '***' : 'empty',
257
+ storachaProof: storachaProof ? '***' : 'empty',
258
+ selectedSpace: selectedSpace?.did() || 'none'
259
+ }, '🔍 Form values');
260
+
261
+ // If using stored credentials, load them
262
+ if (useStoredCredentials) {
263
+ logger.info('🔄 Loading stored credentials for auto-login...');
264
+ const stored = loadCredentials();
265
+ if (!stored) {
266
+ logger.info('⚠️ Auto-login failed: no stored credentials');
267
+ showMessage('No stored credentials found', 'error');
268
+ return;
269
+ }
270
+ keyToUse = stored.key;
271
+ proofToUse = stored.proof;
272
+ logger.info('✅ Loaded stored credentials successfully');
273
+ } else {
274
+ logger.info('🔐 Manual login with form credentials');
275
+ }
276
+
277
+ if (!keyToUse || !proofToUse) {
278
+ showMessage('Please provide both Storacha key and proof', 'error');
279
+ return;
280
+ }
281
+
282
+ isLoading = true;
283
+ status = useStoredCredentials ? 'Auto-logging in...' : 'Logging in...';
284
+
285
+ try {
286
+ client = await initializeStorachaClient(keyToUse, proofToUse);
287
+
288
+ // Initialize the bridge with credentials
289
+ initializeBridge(keyToUse, proofToUse);
290
+
291
+ // For credential-based login, check current space instead of accounts
292
+ currentSpace = client.currentSpace();
293
+ if (currentSpace) {
294
+ // Credential-based login successful
295
+ isLoggedIn = true;
296
+
297
+ // Save credentials for future auto-login (only if not already stored)
298
+ if (!useStoredCredentials) {
299
+ saveCredentials(keyToUse, proofToUse);
300
+ showMessage('Successfully logged in to Storacha! Credentials saved for auto-login.');
301
+ } else {
302
+ showMessage('Successfully auto-logged in to Storacha!');
303
+ }
304
+
305
+ clearForms();
306
+
307
+ // Load spaces (will handle credential-based client properly)
308
+ await loadSpaces();
309
+ } else {
310
+ // Try traditional account-based approach as fallback
311
+ const accounts = client.accounts();
312
+ if (accounts.length > 0) {
313
+ isLoggedIn = true;
314
+
315
+ if (!useStoredCredentials) {
316
+ saveCredentials(keyToUse, proofToUse);
317
+ showMessage('Successfully logged in to Storacha! Credentials saved for auto-login.');
318
+ } else {
319
+ showMessage('Successfully auto-logged in to Storacha!');
320
+ }
321
+
322
+ clearForms();
323
+ await loadSpaces();
324
+ } else {
325
+ throw new Error('Failed to authenticate - no space or account found');
326
+ }
327
+ }
328
+ } catch (err) {
329
+ showMessage(`Login failed: ${err.message}`, 'error');
330
+ // If auto-login failed, clear stored credentials
331
+ if (useStoredCredentials) {
332
+ clearStoredCredentials();
333
+ }
334
+ } finally {
335
+ isLoading = false;
336
+ status = '';
337
+ }
338
+ }
339
+
340
+ // Logout
341
+ function handleLogout() {
342
+ isLoggedIn = false;
343
+ client = null;
344
+ currentSpace = null;
345
+ spaces = [];
346
+ clearForms();
347
+ clearStoredCredentials(); // Clear stored credentials on logout
348
+
349
+ // Clean up bridge
350
+ if (bridge) {
351
+ bridge.removeAllListeners();
352
+ bridge = null;
353
+ }
354
+
355
+ resetProgress();
356
+ showMessage('Logged out successfully');
357
+ }
358
+
359
+ // Load spaces
360
+ async function loadSpaces() {
361
+ if (!client) return;
362
+
363
+ isLoading = true;
364
+ status = 'Loading spaces...';
365
+
366
+ try {
367
+ spaces = await listSpaces(client);
368
+ logger.info({ count: spaces.length, spaces: spaces.map(s => s.did()) }, '📋 Available spaces loaded');
369
+ } catch (err) {
370
+ showMessage(`Failed to load spaces: ${err.message}`, 'error');
371
+ } finally {
372
+ isLoading = false;
373
+ status = '';
374
+ }
375
+ }
376
+
377
+ // Create new space
378
+ async function handleCreateSpace() {
379
+ if (!newSpaceName.trim()) {
380
+ showMessage('Please enter a space name', 'error');
381
+ return;
382
+ }
383
+
384
+ isLoading = true;
385
+ status = 'Creating space...';
386
+
387
+ try {
388
+ const result = await createSpace(client, newSpaceName.trim());
389
+
390
+ if (result.success) {
391
+ showMessage(`Space "${newSpaceName}" created successfully!`);
392
+ newSpaceName = '';
393
+ await loadSpaces(); // Reload spaces
394
+ } else {
395
+ showMessage(result.error, 'error');
396
+ }
397
+ } catch (err) {
398
+ showMessage(`Failed to create space: ${err.message}`, 'error');
399
+ } finally {
400
+ isLoading = false;
401
+ status = '';
402
+ }
403
+ }
404
+
405
+ // Set current space
406
+ async function selectSpace(space) {
407
+ isLoading = true;
408
+ status = 'Switching space...';
409
+
410
+ try {
411
+ await client.setCurrentSpace(space.did);
412
+ currentSpace = space;
413
+ showMessage(`Switched to space: ${space.name}`);
414
+ } catch (err) {
415
+ showMessage(`Failed to switch space: ${err.message}`, 'error');
416
+ } finally {
417
+ isLoading = false;
418
+ status = '';
419
+ }
420
+ }
421
+
422
+ // Backup database with progress tracking
423
+ async function handleBackup() {
424
+ if (!bridge) {
425
+ showMessage('Please log in first', 'error');
426
+ return;
427
+ }
428
+
429
+ if (!$initializationStore.isInitialized) {
430
+ showMessage('OrbitDB is not initialized yet', 'error');
431
+ return;
432
+ }
433
+
434
+ if ($todosStore.length === 0) {
435
+ showMessage('No todos to backup', 'error');
436
+ return;
437
+ }
438
+
439
+ isLoading = true;
440
+ resetProgress();
441
+ status = 'Preparing backup...';
442
+
443
+ try {
444
+ logger.info('🚀 Starting backup with real progress tracking...', $todoDBStore);
445
+
446
+ const result = await bridge.backup($orbitDBStore, $todoDBStore.address);
447
+
448
+ if (result.success) {
449
+ showMessage(
450
+ `Backup completed! ${result.blocksUploaded}/${result.blocksTotal} blocks uploaded`
451
+ );
452
+ } else {
453
+ showMessage(result.error, 'error');
454
+ }
455
+ } catch (err) {
456
+ showMessage(`Backup failed: ${err.message}`, 'error');
457
+ } finally {
458
+ isLoading = false;
459
+ status = '';
460
+ resetProgress();
461
+ }
462
+ }
463
+
464
+
465
+
466
+ // Format date
467
+ function formatDate(dateString) {
468
+ return new Date(dateString).toLocaleString();
469
+ }
470
+
471
+ // Format space name
472
+ function formatSpaceName(space) {
473
+ return space.name === 'Unnamed Space' ? `Space ${space.did.slice(-8)}` : space.name;
474
+ }
475
+
476
+
477
+
478
+ // Auto-login on component mount
479
+ onMount(async () => {
480
+ logger.info('🚀 StorachaIntegration component mounted');
481
+
482
+ // Try to auto-login with stored credentials (but don't show error for first-time users)
483
+ const stored = loadCredentials();
484
+ if (stored) {
485
+ logger.info('🔐 Found stored Storacha credentials, attempting auto-login...');
486
+ try {
487
+ await handleCredentialsLogin(true);
488
+ } catch (err) {
489
+ logger.warn({ error: err.message }, '⚠️ Auto-login failed, clearing stored credentials:');
490
+ clearStoredCredentials();
491
+ }
492
+ } else {
493
+ logger.info('🔒 No stored credentials found, user needs to login manually');
494
+ }
495
+ });
496
+
497
+
498
+
499
+ async function restoreFromSpace() {
500
+ if (!$orbitDBStore) {
501
+ showMessage('OrbitDB not initialized. Please wait for initialization to complete.', 'error');
502
+ return;
503
+ }
504
+
505
+ isLoading = true;
506
+ resetProgress();
507
+ status = 'Preparing restore...';
508
+
509
+ try {
510
+ logger.info('🔄 Starting restore from Storacha space with fresh OrbitDB instance...');
511
+
512
+ // Get Storacha credentials
513
+ const storachaKey = localStorage.getItem('storacha_key') || '';
514
+ const storachaProof = localStorage.getItem('storacha_proof') || '';
515
+
516
+ if (!storachaKey || !storachaProof) {
517
+ throw new Error('Storacha credentials not found. Please login to Storacha first.');
518
+ }
519
+
520
+ // Step 1: Create fresh instances for restore
521
+ status = 'Creating fresh OrbitDB instance for restore...';
522
+ logger.info('🔧 Creating fresh OrbitDB instance for restore...');
523
+
524
+ // Create unique libp2p config
525
+ const config = await createLibp2pConfig({
526
+ enablePeerConnections: false,
527
+ enableNetworkConnection: false,
528
+ enablePersistentStorage: true
529
+ });
530
+
531
+
532
+ // Create fresh Helia instance with persistent initializeDatabasestorage
533
+ logger.info('🗄️ Initializing fresh Helia with persistent storage...');
534
+ const blockstore = new LevelBlockstore(`./helia-blocks-restore-${Date.now()}`);
535
+ const datastore = new LevelDatastore(`./helia-data-restore-${Date.now()}`);
536
+ // Helia 7 builds libp2p from options, and fills any of the keys it knows
537
+ // that are left out from its own defaults, which dial the public network.
538
+ const newHelia = await createHelia({
539
+ libp2p: { addresses: { listen: [] }, peerDiscovery: [], services: {}, ...config },
540
+ blockstore,
541
+ datastore
542
+ }).start();
543
+ const newLibp2p = newHelia.libp2p;
544
+ logger.info('✅ Fresh libp2p instance created');
545
+ logger.info('✅ Fresh Helia instance created');
546
+
547
+ // Create fresh OrbitDB instance
548
+ const newOrbitDB = await createOrbitDB({
549
+ ipfs: newHelia,
550
+ id: `restored-instance-${Date.now()}`
551
+ });
552
+ logger.info('✅ Fresh OrbitDB instance created:', newOrbitDB.id);
553
+
554
+ // Step 2: Restore from Storacha backup
555
+ status = 'Restoring database from Storacha backup...';
556
+ logger.info('🔄 Restoring database from Storacha backup...');
557
+
558
+ const result = await restoreDatabaseFromSpace(newOrbitDB, {
559
+ storachaKey,
560
+ storachaProof,
561
+ timeout: 60000
562
+ });
563
+ logger.info('Restore result:', result);
564
+
565
+ if (result.success) {
566
+ // Step 3: Replace the current instances with the restored ones
567
+ status = 'Replacing current database with restored data...';
568
+ logger.info('🔄 Replacing current database with restored data...');
569
+
570
+ // Close existing instances (if any)
571
+ try {
572
+ if ($todoDBStore) {
573
+ await $todoDBStore.close();
574
+ }
575
+ } catch (closeError) {
576
+ logger.warn({ error: closeError.message }, '⚠️ Error closing existing database:');
577
+ }
578
+
579
+ // Update the stores with new instances
580
+ libp2pStore.set(newLibp2p);
581
+ peerIdStore.set(newLibp2p.peerId.toString());
582
+ orbitDBStore.set(newOrbitDB);
583
+
584
+ // Initialize database with the restored database
585
+ await initializeDatabase(newOrbitDB, result.database, {
586
+ enablePersistentStorage: true,
587
+ enableNetworkConnection: true,
588
+ enablePeerConnections: true
589
+ });
590
+
591
+ // Load todos from the restored database
592
+ await loadTodos();
593
+
594
+ showMessage(
595
+ `Database restored successfully! ${result.entriesRecovered} entries recovered from Storacha space. Fresh OrbitDB instance created.`
596
+ );
597
+
598
+ logger.info('🎉 Restore completed with fresh instance:', {
599
+ newOrbitDBId: newOrbitDB.id,
600
+ newPeerId: newLibp2p.peerId.toString(),
601
+ restoredAddress: result.database.address,
602
+ entriesRecovered: result.entriesRecovered
603
+ });
604
+
605
+ } else {
606
+ // If restore failed, cleanup the new instances
607
+ try {
608
+ await newOrbitDB.stop();
609
+ await newHelia.stop();
610
+ await newLibp2p.stop();
611
+ } catch (cleanupError) {
612
+ logger.warn({ error: cleanupError.message }, '⚠️ Error cleaning up failed restore instances:');
613
+ }
614
+ showMessage(`Restore failed: ${result.error}`, 'error');
615
+ }
616
+
617
+ } catch (error) {
618
+ logger.error({ error }, '❌ Restore failed:');
619
+ showMessage(`Restore failed: ${error.message}`, 'error');
620
+ } finally {
621
+ isLoading = false;
622
+ status = '';
623
+ resetProgress();
624
+ }
625
+ }
626
+ </script>
627
+
628
+ <div
629
+ class="mt-6 rounded-lg border border-blue-200 bg-gradient-to-r from-blue-50 to-indigo-50 p-4 dark:border-gray-600 dark:from-gray-700 dark:to-gray-600"
630
+ >
631
+ <!-- Header -->
632
+ <div class="mb-4 flex items-center justify-between">
633
+ <div class="flex items-center space-x-2">
634
+ <Cloud class="h-5 w-5 text-blue-600 dark:text-blue-400" />
635
+ <h3 class="text-lg font-semibold text-gray-800 dark:text-white">Storacha Integration</h3>
636
+ </div>
637
+
638
+ <button
639
+ on:click={() => (showStoracha = !showStoracha)}
640
+ class="text-blue-600 transition-colors hover:text-blue-800 dark:text-blue-400 dark:hover:text-blue-300"
641
+ >
642
+ {showStoracha ? 'Hide' : 'Show'}
643
+ </button>
644
+ </div>
645
+
646
+ {#if showStoracha}
647
+ <!-- Status Messages -->
648
+ {#if error}
649
+ <div
650
+ class="mb-4 rounded-md border border-red-300 bg-red-100 p-3 dark:border-red-600 dark:bg-red-900/30"
651
+ >
652
+ <div class="flex items-center space-x-2">
653
+ <AlertCircle class="h-4 w-4 text-red-600 dark:text-red-400" />
654
+ <span class="text-sm text-red-700 dark:text-red-300">{error}</span>
655
+ </div>
656
+ </div>
657
+ {/if}
658
+
659
+ {#if success}
660
+ <div
661
+ class="mb-4 rounded-md border border-green-300 bg-green-100 p-3 dark:border-green-600 dark:bg-green-900/30"
662
+ >
663
+ <div class="flex items-center space-x-2">
664
+ <CheckCircle class="h-4 w-4 text-green-600 dark:text-green-400" />
665
+ <span class="text-sm text-green-700 dark:text-green-300">{success}</span>
666
+ </div>
667
+ </div>
668
+ {/if}
669
+
670
+ {#if status}
671
+ <div
672
+ class="mb-4 rounded-md border border-blue-300 bg-blue-100 p-3 dark:border-blue-600 dark:bg-blue-900/30"
673
+ >
674
+ <div class="flex items-center space-x-2">
675
+ <Loader2 class="h-4 w-4 animate-spin text-blue-600 dark:text-blue-400" />
676
+ <span class="text-sm text-blue-700 dark:text-blue-300">{status}</span>
677
+ </div>
678
+ </div>
679
+ {/if}
680
+
681
+ <!-- Progress Bar -->
682
+ {#if showProgress}
683
+ <div
684
+ class="mb-4 rounded-md border border-purple-300 bg-purple-100 p-3 dark:border-purple-600 dark:bg-purple-900/30"
685
+ >
686
+ <div class="mb-2 flex items-center justify-between text-sm">
687
+ <span class="font-medium text-purple-800 dark:text-purple-200">
688
+ {progressType === 'upload' ? 'Uploading' : 'Downloading'} Progress
689
+ </span>
690
+ <span class="text-purple-700 dark:text-purple-300">
691
+ {progressPercentage}% ({progressCurrent}/{progressTotal})
692
+ </span>
693
+ </div>
694
+ <div class="h-2 w-full rounded-full bg-purple-200 dark:bg-purple-700">
695
+ <div
696
+ class="h-2 rounded-full bg-purple-600 transition-all duration-300 ease-out dark:bg-purple-400"
697
+ style="width: {progressPercentage}%"
698
+ ></div>
699
+ </div>
700
+ {#if progressCurrentBlock}
701
+ <div class="mt-1 font-mono text-xs text-purple-600 dark:text-purple-400">
702
+ {progressType === 'upload' ? 'Current block hash:' : 'Current CID:'}
703
+ {progressType === 'upload'
704
+ ? progressCurrentBlock.hash?.slice(0, 16)
705
+ : progressCurrentBlock.storachaCID?.slice(0, 16)}...
706
+ </div>
707
+ {/if}
708
+ {#if progressError}
709
+ <div class="mt-1 text-xs text-red-600 dark:text-red-400">
710
+ Error: {progressError.message}
711
+ </div>
712
+ {/if}
713
+ </div>
714
+ {/if}
715
+
716
+ {#if !isLoggedIn}
717
+ <!-- Login Section -->
718
+ <div class="space-y-4">
719
+ <div class="text-center text-sm text-gray-600 dark:text-gray-300">
720
+ Connect to Storacha to backup your todos to decentralized storage
721
+ </div>
722
+
723
+ <div class="grid grid-cols-1 gap-3 md:grid-cols-2">
724
+ <button
725
+ on:click={() => {
726
+ clearForms();
727
+ showCreateForm = true;
728
+ }}
729
+ disabled={isLoading}
730
+ class="flex items-center justify-center space-x-2 rounded-md bg-green-600 px-4 py-2 text-white transition-colors hover:bg-green-700 disabled:opacity-50"
731
+ >
732
+ <Mail class="h-4 w-4" />
733
+ <span>Create New Account</span>
734
+ </button>
735
+
736
+ <button
737
+ on:click={() => {
738
+ clearForms();
739
+ showCredentialsForm = true;
740
+ }}
741
+ disabled={isLoading}
742
+ class="flex items-center justify-center space-x-2 rounded-md bg-blue-600 px-4 py-2 text-white transition-colors hover:bg-blue-700 disabled:opacity-50"
743
+ >
744
+ <Key class="h-4 w-4" />
745
+ <span>Login with Credentials</span>
746
+ </button>
747
+ </div>
748
+
749
+ <!-- Create Account Form -->
750
+ {#if showCreateForm}
751
+ <div class="rounded-md border bg-white p-4 dark:bg-gray-700">
752
+ <h4 class="text-md mb-3 font-medium text-gray-800 dark:text-white">
753
+ Create New Storacha Account
754
+ </h4>
755
+ <div class="space-y-3">
756
+ <input
757
+ bind:value={email}
758
+ type="email"
759
+ placeholder="Enter your email address"
760
+ class="w-full rounded-md border border-gray-300 bg-white px-3 py-2 text-gray-900 focus:border-transparent focus:ring-2 focus:ring-blue-500 focus:outline-none dark:border-gray-600 dark:bg-gray-800 dark:text-white"
761
+ />
762
+ <div class="flex space-x-2">
763
+ <button
764
+ on:click={handleEmailLogin}
765
+ disabled={isLoading || !email.trim()}
766
+ class="flex-1 rounded-md bg-green-600 px-4 py-2 text-white transition-colors hover:bg-green-700 disabled:opacity-50"
767
+ >
768
+ {isLoading ? 'Creating...' : 'Create Account'}
769
+ </button>
770
+ <button
771
+ on:click={clearForms}
772
+ class="rounded-md bg-gray-300 px-4 py-2 text-gray-700 transition-colors hover:bg-gray-400 dark:bg-gray-600 dark:text-gray-200 dark:hover:bg-gray-500"
773
+ >
774
+ Cancel
775
+ </button>
776
+ </div>
777
+ </div>
778
+ </div>
779
+ {/if}
780
+
781
+ <!-- Credentials Form -->
782
+ {#if showCredentialsForm}
783
+ <div class="rounded-md border bg-white p-4 dark:bg-gray-700">
784
+ <h4 class="text-md mb-3 font-medium text-gray-800 dark:text-white">
785
+ Login with Storacha Credentials
786
+ </h4>
787
+ <div class="space-y-3">
788
+ <input
789
+ bind:value={storachaKey}
790
+ type="password"
791
+ placeholder="Storacha Private Key"
792
+ class="w-full rounded-md border border-gray-300 bg-white px-3 py-2 font-mono text-sm text-gray-900 focus:border-transparent focus:ring-2 focus:ring-blue-500 focus:outline-none dark:border-gray-600 dark:bg-gray-800 dark:text-white"
793
+ />
794
+ <textarea
795
+ bind:value={storachaProof}
796
+ placeholder="Storacha Proof (delegation)"
797
+ rows="3"
798
+ class="w-full resize-none rounded-md border border-gray-300 bg-white px-3 py-2 font-mono text-xs text-gray-900 focus:border-transparent focus:ring-2 focus:ring-blue-500 focus:outline-none dark:border-gray-600 dark:bg-gray-800 dark:text-white"
799
+ ></textarea>
800
+ <div class="flex space-x-2">
801
+ <button
802
+ on:click={() => handleCredentialsLogin()}
803
+ disabled={isLoading || !storachaKey.trim() || !storachaProof.trim()}
804
+ class="flex-1 rounded-md bg-blue-600 px-4 py-2 text-white transition-colors hover:bg-blue-700 disabled:opacity-50"
805
+ >
806
+ {isLoading ? 'Logging in...' : 'Login'}
807
+ </button>
808
+ <button
809
+ on:click={clearForms}
810
+ class="rounded-md bg-gray-300 px-4 py-2 text-gray-700 transition-colors hover:bg-gray-400 dark:bg-gray-600 dark:text-gray-200 dark:hover:bg-gray-500"
811
+ >
812
+ Cancel
813
+ </button>
814
+ </div>
815
+ </div>
816
+ </div>
817
+ {/if}
818
+ </div>
819
+ {:else}
820
+ <!-- Logged In Section -->
821
+ <div class="space-y-4">
822
+ <!-- Account Info -->
823
+ <div
824
+ class="flex items-center justify-between rounded-md border bg-white p-3 dark:bg-gray-700"
825
+ >
826
+ <div class="flex items-center space-x-3">
827
+ <div
828
+ class="flex h-8 w-8 items-center justify-center rounded-full bg-green-100 dark:bg-green-900/30"
829
+ >
830
+ <CheckCircle class="h-4 w-4 text-green-600 dark:text-green-400" />
831
+ </div>
832
+ <div>
833
+ <div class="text-sm font-medium text-gray-800 dark:text-white">
834
+ Connected to Storacha
835
+ </div>
836
+ {#if currentSpace}
837
+ <div class="text-xs text-gray-500 dark:text-gray-400">
838
+ Current space: {formatSpaceName(currentSpace)}
839
+ </div>
840
+ {/if}
841
+ </div>
842
+ </div>
843
+
844
+ <button
845
+ on:click={handleLogout}
846
+ class="flex items-center space-x-1 px-3 py-1 text-sm text-red-600 transition-colors hover:text-red-800 dark:text-red-400 dark:hover:text-red-300"
847
+ >
848
+ <LogOut class="h-3 w-3" />
849
+ <span>Logout</span>
850
+ </button>
851
+ </div>
852
+
853
+ <!-- Action Buttons -->
854
+ <div class="grid grid-cols-1 gap-3 md:grid-cols-2">
855
+ <button
856
+ on:click={handleBackup}
857
+ disabled={isLoading || !$initializationStore.isInitialized || $todosStore.length === 0}
858
+ class="flex items-center justify-center space-x-2 rounded-md bg-green-600 px-4 py-2 text-white transition-colors hover:bg-green-700 disabled:opacity-50"
859
+ >
860
+ <Upload class="h-4 w-4" />
861
+ <span>Backup Database</span>
862
+ </button>
863
+
864
+ <button
865
+ on:click={restoreFromSpace}
866
+ disabled={isLoading || !$initializationStore.isInitialized}
867
+ class="flex items-center justify-center space-x-2 rounded-md bg-orange-600 px-4 py-2 text-white transition-colors hover:bg-orange-700 disabled:opacity-50"
868
+ >
869
+ <Download class="h-4 w-4" />
870
+ <span>Restore Database</span>
871
+ </button>
872
+ </div>
873
+
874
+ <!-- Spaces Management -->
875
+ <div class="rounded-md border bg-white p-4 dark:bg-gray-700">
876
+ <h4
877
+ class="text-md mb-3 flex items-center space-x-2 font-medium text-gray-800 dark:text-white"
878
+ >
879
+ <List class="h-4 w-4" />
880
+ <span>Spaces ({spaces.length})</span>
881
+ </h4>
882
+
883
+ <!-- Create New Space -->
884
+ <div class="mb-4 flex space-x-2">
885
+ <input
886
+ bind:value={newSpaceName}
887
+ placeholder="New space name"
888
+ class="flex-1 rounded-md border border-gray-300 bg-white px-3 py-2 text-sm text-gray-900 focus:border-transparent focus:ring-2 focus:ring-blue-500 focus:outline-none dark:border-gray-600 dark:bg-gray-800 dark:text-white"
889
+ />
890
+ <button
891
+ on:click={handleCreateSpace}
892
+ disabled={isLoading || !newSpaceName.trim()}
893
+ class="rounded-md bg-green-600 px-3 py-2 text-white transition-colors hover:bg-green-700 disabled:opacity-50"
894
+ >
895
+ <Plus class="h-4 w-4" />
896
+ </button>
897
+ </div>
898
+
899
+ <!-- Spaces List -->
900
+ {#if spaces.length === 0}
901
+ <div class="py-4 text-center text-sm text-gray-500 dark:text-gray-400">
902
+ No spaces found
903
+ </div>
904
+ {:else}
905
+ <div class="max-h-40 space-y-2 overflow-y-auto">
906
+ {#each spaces as space (space.did)}
907
+ <div
908
+ class="flex items-center justify-between rounded border bg-gray-50 p-2 dark:bg-gray-600"
909
+ >
910
+ <div>
911
+ <div class="text-sm font-medium text-gray-800 dark:text-white">
912
+ {formatSpaceName(space)}
913
+ </div>
914
+ <div class="font-mono text-xs text-gray-500 dark:text-gray-400">
915
+ {space.did.slice(0, 20)}...
916
+ </div>
917
+ </div>
918
+ {#if currentSpace?.did !== space.did}
919
+ <button
920
+ on:click={() => selectSpace(space)}
921
+ class="px-2 py-1 text-sm text-blue-600 transition-colors hover:text-blue-800 dark:text-blue-400 dark:hover:text-blue-300"
922
+ >
923
+ Select
924
+ </button>
925
+ {:else}
926
+ <span
927
+ class="px-2 py-1 text-sm font-medium text-green-600 dark:text-green-400"
928
+ >
929
+ Current
930
+ </span>
931
+ {/if}
932
+ </div>
933
+ {/each}
934
+ </div>
935
+ {/if}
936
+ </div>
937
+
938
+
939
+ </div>
940
+ {/if}
941
+ {/if}
942
+ </div>
943
+
944
+
945
+
946
+ <style>
947
+ /* Custom scrollbar for webkit browsers */
948
+ .overflow-y-auto::-webkit-scrollbar {
949
+ width: 4px;
950
+ }
951
+
952
+ .overflow-y-auto::-webkit-scrollbar-track {
953
+ background: rgba(0, 0, 0, 0.1);
954
+ border-radius: 2px;
955
+ }
956
+
957
+ .overflow-y-auto::-webkit-scrollbar-thumb {
958
+ background: rgba(0, 0, 0, 0.3);
959
+ border-radius: 2px;
960
+ }
961
+
962
+ .overflow-y-auto::-webkit-scrollbar-thumb:hover {
963
+ background: rgba(0, 0, 0, 0.4);
964
+ }
965
+ </style>