create-sia-app 0.1.13 → 0.1.15
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.
- package/package.json +1 -1
- package/template/src/components/CopyButton.tsx +1 -1
- package/template/src/components/DevNote.tsx +4 -4
- package/template/src/components/Navbar.tsx +5 -5
- package/template/src/components/Toast.tsx +1 -1
- package/template/src/components/auth/ApproveScreen.tsx +10 -10
- package/template/src/components/auth/AuthFlow.tsx +2 -2
- package/template/src/components/auth/ConnectScreen.tsx +6 -6
- package/template/src/components/auth/LoadingScreen.tsx +2 -2
- package/template/src/components/auth/RecoveryScreen.tsx +13 -13
- package/template/src/components/upload/UploadZone.tsx +46 -34
- package/template/src/index.css +2 -2
- package/template/src/lib/constants.ts +5 -1
- package/template/src/stores/auth.ts +2 -1
package/package.json
CHANGED
|
@@ -16,7 +16,7 @@ export function CopyButton({
|
|
|
16
16
|
navigator.clipboard.writeText(value)
|
|
17
17
|
addToast(label)
|
|
18
18
|
}}
|
|
19
|
-
className="p-1 text-neutral-
|
|
19
|
+
className="p-1 text-neutral-400 hover:text-neutral-700 transition-colors"
|
|
20
20
|
title="Copy"
|
|
21
21
|
>
|
|
22
22
|
<svg
|
|
@@ -8,12 +8,12 @@ export function DevNote({
|
|
|
8
8
|
children: ReactNode
|
|
9
9
|
}) {
|
|
10
10
|
return (
|
|
11
|
-
<div className="border-l-4 border-amber-500 bg-amber-
|
|
12
|
-
<p className="text-amber-
|
|
11
|
+
<div className="border-l-4 border-amber-500 bg-amber-50 rounded-r-lg p-4 space-y-1">
|
|
12
|
+
<p className="text-amber-700 text-xs font-semibold uppercase tracking-wider">
|
|
13
13
|
Developer Note
|
|
14
14
|
</p>
|
|
15
|
-
<p className="text-amber-
|
|
16
|
-
<div className="text-amber-
|
|
15
|
+
<p className="text-amber-900 text-sm font-medium">{title}</p>
|
|
16
|
+
<div className="text-amber-900/80 text-xs leading-relaxed">
|
|
17
17
|
{children}
|
|
18
18
|
</div>
|
|
19
19
|
</div>
|
|
@@ -23,16 +23,16 @@ export function Navbar() {
|
|
|
23
23
|
}
|
|
24
24
|
|
|
25
25
|
return (
|
|
26
|
-
<header className="border-b border-neutral-
|
|
26
|
+
<header className="border-b border-neutral-200/80">
|
|
27
27
|
<div className="flex items-center justify-between px-6 py-3 max-w-5xl mx-auto">
|
|
28
|
-
<h1 className="text-sm font-semibold text-
|
|
28
|
+
<h1 className="text-sm font-semibold text-neutral-900 tracking-tight">
|
|
29
29
|
{APP_NAME}
|
|
30
30
|
</h1>
|
|
31
31
|
{isConnected && publicKey && (
|
|
32
32
|
<div className="flex items-center gap-3">
|
|
33
33
|
<span className="relative flex h-2 w-2">
|
|
34
|
-
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-green-
|
|
35
|
-
<span className="relative inline-flex rounded-full h-2 w-2 bg-green-
|
|
34
|
+
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-green-500 opacity-75" />
|
|
35
|
+
<span className="relative inline-flex rounded-full h-2 w-2 bg-green-600" />
|
|
36
36
|
</span>
|
|
37
37
|
<span
|
|
38
38
|
className="text-[11px] font-mono text-neutral-500"
|
|
@@ -44,7 +44,7 @@ export function Navbar() {
|
|
|
44
44
|
<button
|
|
45
45
|
type="button"
|
|
46
46
|
onClick={handleSignOut}
|
|
47
|
-
className="text-xs text-neutral-500 hover:text-neutral-
|
|
47
|
+
className="text-xs text-neutral-500 hover:text-neutral-900 transition-colors ml-1"
|
|
48
48
|
>
|
|
49
49
|
Sign Out
|
|
50
50
|
</button>
|
|
@@ -10,7 +10,7 @@ export function Toasts() {
|
|
|
10
10
|
{toasts.map((toast) => (
|
|
11
11
|
<div
|
|
12
12
|
key={toast.id}
|
|
13
|
-
className="animate-fade-in px-4 py-2 bg-
|
|
13
|
+
className="animate-fade-in px-4 py-2 bg-white border border-neutral-200 rounded-lg text-sm text-neutral-700 shadow-lg"
|
|
14
14
|
>
|
|
15
15
|
{toast.message}
|
|
16
16
|
</div>
|
|
@@ -63,10 +63,10 @@ export function ApproveScreen({
|
|
|
63
63
|
<div className="flex flex-col items-center justify-center flex-1 px-4">
|
|
64
64
|
<div className="w-full max-w-md space-y-6">
|
|
65
65
|
<div className="text-center space-y-2">
|
|
66
|
-
<h1 className="text-2xl font-semibold text-
|
|
66
|
+
<h1 className="text-2xl font-semibold text-neutral-900">
|
|
67
67
|
Approve Connection
|
|
68
68
|
</h1>
|
|
69
|
-
<p className="text-neutral-
|
|
69
|
+
<p className="text-neutral-600 text-sm">
|
|
70
70
|
Open the link below to approve this app, then return here.
|
|
71
71
|
</p>
|
|
72
72
|
</div>
|
|
@@ -76,15 +76,15 @@ export function ApproveScreen({
|
|
|
76
76
|
The user must visit the approval URL in another tab (or on the
|
|
77
77
|
indexer's dashboard) to authorize your app. This is an
|
|
78
78
|
out-of-band step — your app polls for approval via{' '}
|
|
79
|
-
<code className="text-amber-
|
|
79
|
+
<code className="text-amber-700">builder.waitForApproval()</code>.
|
|
80
80
|
Once approved, the flow continues to recovery phrase setup.
|
|
81
81
|
</p>
|
|
82
82
|
</DevNote>
|
|
83
83
|
|
|
84
84
|
{approvalUrl && (
|
|
85
85
|
<div className="space-y-3">
|
|
86
|
-
<div className="flex items-center gap-2 p-3 bg-
|
|
87
|
-
<span className="flex-1 text-sm font-mono text-neutral-
|
|
86
|
+
<div className="flex items-center gap-2 p-3 bg-white border border-neutral-300 rounded-lg">
|
|
87
|
+
<span className="flex-1 text-sm font-mono text-neutral-600 truncate">
|
|
88
88
|
{approvalUrl}
|
|
89
89
|
</span>
|
|
90
90
|
<CopyButton value={approvalUrl} label="URL copied" />
|
|
@@ -104,11 +104,11 @@ export function ApproveScreen({
|
|
|
104
104
|
type="button"
|
|
105
105
|
onClick={handleManualCheck}
|
|
106
106
|
disabled={manualChecking}
|
|
107
|
-
className="w-full py-3 bg-neutral-
|
|
107
|
+
className="w-full py-3 bg-neutral-100 hover:bg-neutral-200 disabled:bg-neutral-200 disabled:text-neutral-400 text-neutral-900 font-medium rounded-lg transition-colors"
|
|
108
108
|
>
|
|
109
109
|
{manualChecking ? (
|
|
110
110
|
<span className="flex items-center justify-center gap-2">
|
|
111
|
-
<span className="w-4 h-4 border-2 border-neutral-
|
|
111
|
+
<span className="w-4 h-4 border-2 border-neutral-300 border-t-neutral-900 rounded-full animate-spin" />
|
|
112
112
|
Checking...
|
|
113
113
|
</span>
|
|
114
114
|
) : (
|
|
@@ -116,12 +116,12 @@ export function ApproveScreen({
|
|
|
116
116
|
)}
|
|
117
117
|
</button>
|
|
118
118
|
|
|
119
|
-
<div className="flex items-center justify-center gap-2 text-xs text-neutral-
|
|
119
|
+
<div className="flex items-center justify-center gap-2 text-xs text-neutral-500">
|
|
120
120
|
{polling ? (
|
|
121
121
|
<>
|
|
122
122
|
<span className="relative flex h-1.5 w-1.5">
|
|
123
|
-
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-green-
|
|
124
|
-
<span className="relative inline-flex rounded-full h-1.5 w-1.5 bg-green-
|
|
123
|
+
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-green-500 opacity-75" />
|
|
124
|
+
<span className="relative inline-flex rounded-full h-1.5 w-1.5 bg-green-600" />
|
|
125
125
|
</span>
|
|
126
126
|
Polling for approval...
|
|
127
127
|
</>
|
|
@@ -54,12 +54,12 @@ export function AuthFlow() {
|
|
|
54
54
|
return (
|
|
55
55
|
<div className="flex-1 flex flex-col">
|
|
56
56
|
{error && (
|
|
57
|
-
<div className="fixed top-4 left-1/2 -translate-x-1/2 z-50 px-4 py-2 bg-red-
|
|
57
|
+
<div className="fixed top-4 left-1/2 -translate-x-1/2 z-50 px-4 py-2 bg-red-50 border border-red-200 rounded-lg text-red-800 text-sm max-w-md text-center shadow-sm">
|
|
58
58
|
{error}
|
|
59
59
|
<button
|
|
60
60
|
type="button"
|
|
61
61
|
onClick={() => setError(null)}
|
|
62
|
-
className="ml-2 text-red-
|
|
62
|
+
className="ml-2 text-red-600 hover:text-red-900"
|
|
63
63
|
>
|
|
64
64
|
Dismiss
|
|
65
65
|
</button>
|
|
@@ -45,10 +45,10 @@ export function ConnectScreen({
|
|
|
45
45
|
<div className="flex flex-col items-center justify-center flex-1 px-4">
|
|
46
46
|
<div className="w-full max-w-md space-y-6">
|
|
47
47
|
<div className="text-center space-y-2">
|
|
48
|
-
<h1 className="text-2xl font-semibold text-
|
|
48
|
+
<h1 className="text-2xl font-semibold text-neutral-900">
|
|
49
49
|
Connect to Indexer
|
|
50
50
|
</h1>
|
|
51
|
-
<p className="text-neutral-
|
|
51
|
+
<p className="text-neutral-600 text-sm">
|
|
52
52
|
Enter your Sia indexer URL to get started
|
|
53
53
|
</p>
|
|
54
54
|
</div>
|
|
@@ -56,9 +56,9 @@ export function ConnectScreen({
|
|
|
56
56
|
<DevNote title="Indexer URL & App Key">
|
|
57
57
|
<p>
|
|
58
58
|
The indexer URL points to your Sia storage provider. The default is{' '}
|
|
59
|
-
<code className="text-amber-
|
|
59
|
+
<code className="text-amber-700">https://sia.storage</code>. Your
|
|
60
60
|
app key (set in{' '}
|
|
61
|
-
<code className="text-amber-
|
|
61
|
+
<code className="text-amber-700">src/lib/constants.ts</code>)
|
|
62
62
|
uniquely identifies your app to the indexer.
|
|
63
63
|
</p>
|
|
64
64
|
<p className="mt-1">
|
|
@@ -73,14 +73,14 @@ export function ConnectScreen({
|
|
|
73
73
|
value={url}
|
|
74
74
|
onChange={(e) => setUrl(e.target.value)}
|
|
75
75
|
placeholder="https://sia.storage"
|
|
76
|
-
className="w-full px-4 py-3 bg-
|
|
76
|
+
className="w-full px-4 py-3 bg-white border border-neutral-300 rounded-lg text-neutral-900 placeholder-neutral-400 focus:outline-none focus:border-green-600"
|
|
77
77
|
/>
|
|
78
78
|
|
|
79
79
|
<button
|
|
80
80
|
type="button"
|
|
81
81
|
onClick={handleConnect}
|
|
82
82
|
disabled={loading || !url}
|
|
83
|
-
className="w-full py-3 bg-green-600 hover:bg-green-700 disabled:bg-neutral-
|
|
83
|
+
className="w-full py-3 bg-green-600 hover:bg-green-700 disabled:bg-neutral-200 disabled:text-neutral-400 text-white font-medium rounded-lg transition-colors"
|
|
84
84
|
>
|
|
85
85
|
{loading ? 'Connecting...' : 'Connect'}
|
|
86
86
|
</button>
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
export function LoadingScreen({ message }: { message?: string }) {
|
|
2
2
|
return (
|
|
3
3
|
<div className="flex flex-col items-center justify-center flex-1 gap-4">
|
|
4
|
-
<div className="w-8 h-8 border-2 border-neutral-
|
|
5
|
-
<p className="text-neutral-
|
|
4
|
+
<div className="w-8 h-8 border-2 border-neutral-300 border-t-green-600 rounded-full animate-spin" />
|
|
5
|
+
<p className="text-neutral-500 text-sm">{message || 'Initializing...'}</p>
|
|
6
6
|
</div>
|
|
7
7
|
)
|
|
8
8
|
}
|
|
@@ -71,10 +71,10 @@ export function RecoveryScreen({
|
|
|
71
71
|
<div className="flex flex-col items-center justify-center flex-1 px-4">
|
|
72
72
|
<div className="w-full max-w-md space-y-6">
|
|
73
73
|
<div className="text-center space-y-2">
|
|
74
|
-
<h1 className="text-2xl font-semibold text-
|
|
74
|
+
<h1 className="text-2xl font-semibold text-neutral-900">
|
|
75
75
|
Recovery Phrase
|
|
76
76
|
</h1>
|
|
77
|
-
<p className="text-neutral-
|
|
77
|
+
<p className="text-neutral-600 text-sm">
|
|
78
78
|
Generate a new recovery phrase or enter an existing one.
|
|
79
79
|
</p>
|
|
80
80
|
</div>
|
|
@@ -100,7 +100,7 @@ export function RecoveryScreen({
|
|
|
100
100
|
<button
|
|
101
101
|
type="button"
|
|
102
102
|
onClick={() => setMode('import')}
|
|
103
|
-
className="w-full py-3 bg-neutral-
|
|
103
|
+
className="w-full py-3 bg-neutral-100 hover:bg-neutral-200 text-neutral-900 font-medium rounded-lg transition-colors"
|
|
104
104
|
>
|
|
105
105
|
Enter Existing Phrase
|
|
106
106
|
</button>
|
|
@@ -114,12 +114,12 @@ export function RecoveryScreen({
|
|
|
114
114
|
<div className="flex flex-col items-center justify-center flex-1 px-4">
|
|
115
115
|
<div className="w-full max-w-md space-y-6">
|
|
116
116
|
<div className="text-center space-y-2">
|
|
117
|
-
<h1 className="text-2xl font-semibold text-
|
|
117
|
+
<h1 className="text-2xl font-semibold text-neutral-900">
|
|
118
118
|
{mode === 'generate'
|
|
119
119
|
? 'Save Your Recovery Phrase'
|
|
120
120
|
: 'Enter Recovery Phrase'}
|
|
121
121
|
</h1>
|
|
122
|
-
<p className="text-neutral-
|
|
122
|
+
<p className="text-neutral-600 text-sm">
|
|
123
123
|
{mode === 'generate'
|
|
124
124
|
? 'Write down these 12 words in order. You will need them to recover your account.'
|
|
125
125
|
: 'Enter your 12-word recovery phrase.'}
|
|
@@ -128,14 +128,14 @@ export function RecoveryScreen({
|
|
|
128
128
|
|
|
129
129
|
{mode === 'generate' ? (
|
|
130
130
|
<div className="space-y-2">
|
|
131
|
-
<div className="grid grid-cols-3 gap-2 p-4 bg-
|
|
131
|
+
<div className="grid grid-cols-3 gap-2 p-4 bg-white rounded-lg border border-neutral-300">
|
|
132
132
|
{generatedPhrase.split(' ').map((word, i) => (
|
|
133
133
|
<div
|
|
134
134
|
key={`${word}-${i}`}
|
|
135
|
-
className="text-center py-2 bg-neutral-
|
|
135
|
+
className="text-center py-2 bg-neutral-100 rounded text-sm"
|
|
136
136
|
>
|
|
137
|
-
<span className="text-neutral-
|
|
138
|
-
<span className="text-
|
|
137
|
+
<span className="text-neutral-400 mr-1">{i + 1}.</span>
|
|
138
|
+
<span className="text-neutral-900">{word}</span>
|
|
139
139
|
</div>
|
|
140
140
|
))}
|
|
141
141
|
</div>
|
|
@@ -153,10 +153,10 @@ export function RecoveryScreen({
|
|
|
153
153
|
onChange={(e) => handleValidatePhrase(e.target.value)}
|
|
154
154
|
placeholder="Enter your 12-word recovery phrase..."
|
|
155
155
|
rows={3}
|
|
156
|
-
className="w-full px-4 py-3 bg-
|
|
156
|
+
className="w-full px-4 py-3 bg-white border border-neutral-300 rounded-lg text-neutral-900 placeholder-neutral-400 focus:outline-none focus:border-green-600"
|
|
157
157
|
/>
|
|
158
158
|
{phraseError && (
|
|
159
|
-
<p className="text-red-
|
|
159
|
+
<p className="text-red-600 text-sm">{phraseError}</p>
|
|
160
160
|
)}
|
|
161
161
|
</div>
|
|
162
162
|
)}
|
|
@@ -165,7 +165,7 @@ export function RecoveryScreen({
|
|
|
165
165
|
type="button"
|
|
166
166
|
onClick={handleRegister}
|
|
167
167
|
disabled={loading || !phrase.trim()}
|
|
168
|
-
className="w-full py-3 bg-green-600 hover:bg-green-700 disabled:bg-neutral-
|
|
168
|
+
className="w-full py-3 bg-green-600 hover:bg-green-700 disabled:bg-neutral-200 disabled:text-neutral-400 text-white font-medium rounded-lg transition-colors"
|
|
169
169
|
>
|
|
170
170
|
{loading ? 'Registering...' : 'Complete Setup'}
|
|
171
171
|
</button>
|
|
@@ -178,7 +178,7 @@ export function RecoveryScreen({
|
|
|
178
178
|
setGeneratedPhrase('')
|
|
179
179
|
setPhraseError(null)
|
|
180
180
|
}}
|
|
181
|
-
className="w-full py-2 text-neutral-
|
|
181
|
+
className="w-full py-2 text-neutral-500 hover:text-neutral-900 text-sm transition-colors"
|
|
182
182
|
>
|
|
183
183
|
Back
|
|
184
184
|
</button>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { useCallback, useEffect, useRef, useState } from 'react'
|
|
2
|
-
import { PinnedObject, type ShardProgress } from 'sia-storage'
|
|
3
|
-
import { APP_KEY } from '../../lib/constants'
|
|
2
|
+
import { encodedSize, PinnedObject, type ShardProgress } from 'sia-storage'
|
|
3
|
+
import { APP_KEY, DATA_SHARDS, PARITY_SHARDS } from '../../lib/constants'
|
|
4
4
|
import { useAuthStore } from '../../stores/auth'
|
|
5
5
|
import { DevNote } from '../DevNote'
|
|
6
6
|
|
|
@@ -37,9 +37,10 @@ type UploadedFile = {
|
|
|
37
37
|
|
|
38
38
|
type UploadProgress = {
|
|
39
39
|
fileName: string
|
|
40
|
+
fileSize: number
|
|
40
41
|
shardsDone: number
|
|
41
42
|
bytesUploaded: number
|
|
42
|
-
|
|
43
|
+
encodedTotal: number
|
|
43
44
|
}
|
|
44
45
|
|
|
45
46
|
type DownloadProgress = {
|
|
@@ -88,11 +89,13 @@ export function UploadZone() {
|
|
|
88
89
|
if (!sdk) return
|
|
89
90
|
setUploading(true)
|
|
90
91
|
setError(null)
|
|
92
|
+
const encodedTotal = encodedSize(file.size, DATA_SHARDS, PARITY_SHARDS)
|
|
91
93
|
setActiveUpload({
|
|
92
94
|
fileName: file.name,
|
|
95
|
+
fileSize: file.size,
|
|
93
96
|
shardsDone: 0,
|
|
94
97
|
bytesUploaded: 0,
|
|
95
|
-
|
|
98
|
+
encodedTotal,
|
|
96
99
|
})
|
|
97
100
|
|
|
98
101
|
try {
|
|
@@ -107,14 +110,17 @@ export function UploadZone() {
|
|
|
107
110
|
let bytesUploaded = 0
|
|
108
111
|
const pinnedObject = await sdk.upload(object, file.stream(), {
|
|
109
112
|
maxInflight: 10,
|
|
113
|
+
dataShards: DATA_SHARDS,
|
|
114
|
+
parityShards: PARITY_SHARDS,
|
|
110
115
|
onShardUploaded: (progress: ShardProgress) => {
|
|
111
116
|
shardsDone++
|
|
112
117
|
bytesUploaded += progress.shardSize
|
|
113
118
|
setActiveUpload({
|
|
114
119
|
fileName: file.name,
|
|
120
|
+
fileSize: file.size,
|
|
115
121
|
shardsDone,
|
|
116
122
|
bytesUploaded,
|
|
117
|
-
|
|
123
|
+
encodedTotal,
|
|
118
124
|
})
|
|
119
125
|
},
|
|
120
126
|
})
|
|
@@ -215,7 +221,7 @@ export function UploadZone() {
|
|
|
215
221
|
? Math.min(
|
|
216
222
|
100,
|
|
217
223
|
Math.round(
|
|
218
|
-
(activeUpload.bytesUploaded / activeUpload.
|
|
224
|
+
(activeUpload.bytesUploaded / activeUpload.encodedTotal) * 100,
|
|
219
225
|
),
|
|
220
226
|
)
|
|
221
227
|
: 0
|
|
@@ -227,34 +233,34 @@ export function UploadZone() {
|
|
|
227
233
|
<DevNote title="Replace Your App Key">
|
|
228
234
|
<p>
|
|
229
235
|
You're using the template placeholder. Set your own key in{' '}
|
|
230
|
-
<code className="text-amber-
|
|
236
|
+
<code className="text-amber-700">src/lib/constants.ts</code> or
|
|
231
237
|
scaffold a fresh project with{' '}
|
|
232
|
-
<code className="text-amber-
|
|
238
|
+
<code className="text-amber-700">bunx create-sia-app</code>.
|
|
233
239
|
</p>
|
|
234
240
|
</DevNote>
|
|
235
241
|
)}
|
|
236
242
|
|
|
237
243
|
<DevNote title="Upload & Download">
|
|
238
244
|
<p>
|
|
239
|
-
<code className="text-amber-
|
|
245
|
+
<code className="text-amber-700">
|
|
240
246
|
sdk.upload(object, file.stream(), opts)
|
|
241
247
|
</code>{' '}
|
|
242
248
|
encrypts, erasure-codes, and streams shards directly to Sia hosts.{' '}
|
|
243
|
-
<code className="text-amber-
|
|
244
|
-
returns a <code className="text-amber-
|
|
249
|
+
<code className="text-amber-700">sdk.download(object, opts)</code>{' '}
|
|
250
|
+
returns a <code className="text-amber-700">ReadableStream</code> of
|
|
245
251
|
decrypted bytes. Per-shard progress is reported via{' '}
|
|
246
|
-
<code className="text-amber-
|
|
247
|
-
<code className="text-amber-
|
|
252
|
+
<code className="text-amber-700">onShardUploaded</code> /{' '}
|
|
253
|
+
<code className="text-amber-700">onShardDownloaded</code>.
|
|
248
254
|
</p>
|
|
249
255
|
</DevNote>
|
|
250
256
|
|
|
251
257
|
{error && (
|
|
252
|
-
<div className="flex items-center justify-between px-4 py-2.5 bg-red-
|
|
258
|
+
<div className="flex items-center justify-between px-4 py-2.5 bg-red-50 border border-red-200 rounded-lg text-red-800 text-sm">
|
|
253
259
|
<span>{error}</span>
|
|
254
260
|
<button
|
|
255
261
|
type="button"
|
|
256
262
|
onClick={() => setError(null)}
|
|
257
|
-
className="text-red-
|
|
263
|
+
className="text-red-600 hover:text-red-900 text-xs ml-4 shrink-0"
|
|
258
264
|
>
|
|
259
265
|
Dismiss
|
|
260
266
|
</button>
|
|
@@ -274,10 +280,10 @@ export function UploadZone() {
|
|
|
274
280
|
}}
|
|
275
281
|
className={`relative block border-2 border-dashed rounded-xl p-16 text-center transition-all duration-150 ${
|
|
276
282
|
uploading
|
|
277
|
-
? 'border-neutral-
|
|
283
|
+
? 'border-neutral-300 cursor-default'
|
|
278
284
|
: dragOver
|
|
279
|
-
? 'border-green-
|
|
280
|
-
: 'border-neutral-
|
|
285
|
+
? 'border-green-600 bg-green-600/5 cursor-pointer'
|
|
286
|
+
: 'border-neutral-300 hover:border-neutral-400 cursor-pointer'
|
|
281
287
|
}`}
|
|
282
288
|
>
|
|
283
289
|
<input
|
|
@@ -294,30 +300,36 @@ export function UploadZone() {
|
|
|
294
300
|
|
|
295
301
|
{activeUpload ? (
|
|
296
302
|
<div className="space-y-4">
|
|
297
|
-
<p className="text-neutral-
|
|
303
|
+
<p className="text-neutral-700 text-sm">
|
|
298
304
|
Uploading{' '}
|
|
299
|
-
<span className="text-
|
|
305
|
+
<span className="text-neutral-900">{activeUpload.fileName}</span>{' '}
|
|
306
|
+
<span className="text-neutral-500">
|
|
307
|
+
({formatBytes(activeUpload.fileSize)})
|
|
308
|
+
</span>
|
|
300
309
|
</p>
|
|
301
|
-
<div className="w-full max-w-xs mx-auto bg-neutral-
|
|
310
|
+
<div className="w-full max-w-xs mx-auto bg-neutral-200 rounded-full h-1.5 overflow-hidden">
|
|
302
311
|
{activeUpload.shardsDone === 0 ? (
|
|
303
|
-
<div className="bg-green-
|
|
312
|
+
<div className="bg-green-600 h-full rounded-full w-1/4 animate-indeterminate" />
|
|
304
313
|
) : (
|
|
305
314
|
<div
|
|
306
|
-
className="bg-green-
|
|
315
|
+
className="bg-green-600 h-full rounded-full transition-all duration-300"
|
|
307
316
|
style={{ width: `${uploadPercent}%` }}
|
|
308
317
|
/>
|
|
309
318
|
)}
|
|
310
319
|
</div>
|
|
311
|
-
<p className="text-neutral-
|
|
320
|
+
<p className="text-neutral-500 text-xs font-mono">
|
|
312
321
|
{activeUpload.shardsDone} shards ·{' '}
|
|
313
|
-
{formatBytes(
|
|
314
|
-
|
|
322
|
+
{formatBytes(
|
|
323
|
+
(activeUpload.bytesUploaded / activeUpload.encodedTotal) *
|
|
324
|
+
activeUpload.fileSize,
|
|
325
|
+
)}{' '}
|
|
326
|
+
/ {formatBytes(activeUpload.fileSize)}
|
|
315
327
|
</p>
|
|
316
328
|
</div>
|
|
317
329
|
) : (
|
|
318
330
|
<div className="space-y-2">
|
|
319
331
|
<svg
|
|
320
|
-
className="w-8 h-8 mx-auto text-neutral-
|
|
332
|
+
className="w-8 h-8 mx-auto text-neutral-400"
|
|
321
333
|
viewBox="0 0 24 24"
|
|
322
334
|
fill="none"
|
|
323
335
|
stroke="currentColor"
|
|
@@ -327,10 +339,10 @@ export function UploadZone() {
|
|
|
327
339
|
<path d="M12 16V4m0 0l-4 4m4-4l4 4" />
|
|
328
340
|
<path d="M20 16v2a2 2 0 01-2 2H6a2 2 0 01-2-2v-2" />
|
|
329
341
|
</svg>
|
|
330
|
-
<p className="text-neutral-
|
|
342
|
+
<p className="text-neutral-600 text-sm">
|
|
331
343
|
Drop files here or click to browse
|
|
332
344
|
</p>
|
|
333
|
-
<p className="text-neutral-
|
|
345
|
+
<p className="text-neutral-500 text-xs">
|
|
334
346
|
Encrypted end-to-end and stored on the Sia network
|
|
335
347
|
</p>
|
|
336
348
|
</div>
|
|
@@ -343,7 +355,7 @@ export function UploadZone() {
|
|
|
343
355
|
<h2 className="text-xs font-medium text-neutral-500 uppercase tracking-wider">
|
|
344
356
|
{files.length} file{files.length !== 1 ? 's' : ''}
|
|
345
357
|
</h2>
|
|
346
|
-
<div className="divide-y divide-neutral-
|
|
358
|
+
<div className="divide-y divide-neutral-200/80">
|
|
347
359
|
{files.map((file) => {
|
|
348
360
|
const isDownloading = downloading === file.id
|
|
349
361
|
return (
|
|
@@ -352,10 +364,10 @@ export function UploadZone() {
|
|
|
352
364
|
className="flex items-center justify-between py-3 group"
|
|
353
365
|
>
|
|
354
366
|
<div className="flex-1 min-w-0 mr-4">
|
|
355
|
-
<p className="text-sm text-neutral-
|
|
367
|
+
<p className="text-sm text-neutral-900 truncate">
|
|
356
368
|
{file.metadata.name}
|
|
357
369
|
</p>
|
|
358
|
-
<p className="text-xs text-neutral-
|
|
370
|
+
<p className="text-xs text-neutral-500 mt-0.5">
|
|
359
371
|
{formatBytes(file.metadata.size)}
|
|
360
372
|
{file.metadata.type !== 'application/octet-stream' && (
|
|
361
373
|
<span> · {file.metadata.type}</span>
|
|
@@ -376,7 +388,7 @@ export function UploadZone() {
|
|
|
376
388
|
type="button"
|
|
377
389
|
onClick={() => downloadFile(file)}
|
|
378
390
|
disabled={downloading !== null}
|
|
379
|
-
className="text-xs text-neutral-
|
|
391
|
+
className="text-xs text-neutral-500 hover:text-neutral-900 disabled:opacity-30 disabled:cursor-default transition-colors"
|
|
380
392
|
title="Download"
|
|
381
393
|
>
|
|
382
394
|
{isDownloading ? (
|
|
@@ -406,7 +418,7 @@ export function UploadZone() {
|
|
|
406
418
|
)}
|
|
407
419
|
</button>
|
|
408
420
|
<span
|
|
409
|
-
className="text-[11px] text-neutral-
|
|
421
|
+
className="text-[11px] text-neutral-400 font-mono group-hover:text-neutral-700 transition-colors"
|
|
410
422
|
title={file.metadata.hash}
|
|
411
423
|
>
|
|
412
424
|
{file.metadata.hash.slice(0, 8)}...
|
package/template/src/index.css
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { AppMetadata } from 'sia-storage'
|
|
2
2
|
|
|
3
|
-
// biome-ignore format:
|
|
3
|
+
// biome-ignore format: long hex literal
|
|
4
4
|
export const APP_KEY = '{{APP_KEY}}'
|
|
5
5
|
export const APP_NAME = '{{APP_NAME}}'
|
|
6
6
|
export const DEFAULT_INDEXER_URL = '{{INDEXER_URL}}'
|
|
@@ -12,3 +12,7 @@ export const APP_META: AppMetadata = {
|
|
|
12
12
|
logoUrl: undefined,
|
|
13
13
|
callbackUrl: undefined,
|
|
14
14
|
}
|
|
15
|
+
|
|
16
|
+
// Erasure coding parameters — passed to sdk.upload() and encodedSize().
|
|
17
|
+
export const DATA_SHARDS = 10
|
|
18
|
+
export const PARITY_SHARDS = 20
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { Sdk } from 'sia-storage'
|
|
2
2
|
import { create } from 'zustand'
|
|
3
3
|
import { persist } from 'zustand/middleware'
|
|
4
|
+
import { APP_KEY } from '../lib/constants'
|
|
4
5
|
|
|
5
6
|
export type AuthStep =
|
|
6
7
|
| 'loading'
|
|
@@ -50,7 +51,7 @@ export const useAuthStore = create<AuthState>()(
|
|
|
50
51
|
}),
|
|
51
52
|
}),
|
|
52
53
|
{
|
|
53
|
-
name:
|
|
54
|
+
name: `sia-auth-${APP_KEY.slice(0, 16)}`,
|
|
54
55
|
partialize: (state) => ({
|
|
55
56
|
storedKeyHex: state.storedKeyHex,
|
|
56
57
|
indexerUrl: state.indexerUrl,
|