Skip to content

Commit 39d2a18

Browse files
authored
feat: fix upload display & prev uploads on page hit (#26)
* feat: upload progress provides details on complete * feat: display previously uploaded content * chore: some cleanup * fix: showing list always, show drag-n-drop always * fix: ipni check displays error properly * chore: fix lint * chore: update package-lock.json * chore: add debug logging for shared-pieces issue * fix: upload shows above previously uploaded files * fix: latest style updates * fix: upload form always visible * fix: loading screen for prior pieces * chore: fix lint
1 parent 257ef67 commit 39d2a18

File tree

14 files changed

+1752
-288
lines changed

14 files changed

+1752
-288
lines changed

package-lock.json

Lines changed: 1140 additions & 193 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/components/layout/content.tsx

Lines changed: 133 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,68 @@
1-
import { useState } from 'react'
1+
import { useContext, useEffect, useState } from 'react'
2+
import { FilecoinPinContext } from '../../context/filecoin-pin-provider.tsx'
3+
import { useDatasetPieces } from '../../hooks/use-dataset-pieces.ts'
24
import { useFilecoinUpload } from '../../hooks/use-filecoin-upload.ts'
5+
import { formatFileSize } from '../../utils/format-file-size.ts'
6+
import { Heading } from '../ui/heading.tsx'
7+
import { LoadingState } from '../ui/loading-state.tsx'
8+
import { PageTitle } from '../ui/page-title.tsx'
39
import DragNDrop from '../upload/drag-n-drop.tsx'
10+
import type { UploadProgress as UploadProgressType } from '../upload/upload-progress.tsx'
411
import UploadProgress from '../upload/upload-progress.tsx'
512
import './content.css'
6-
import { PageTitle } from '../ui/page-title.tsx'
7-
import { Heading } from '../ui/heading.tsx'
13+
14+
// Completed state for displaying upload history
15+
const COMPLETED_PROGRESS: UploadProgressType[] = [
16+
{ step: 'creating-car', status: 'completed', progress: 100 },
17+
{ step: 'checking-readiness', status: 'completed', progress: 100 },
18+
{ step: 'uploading-car', status: 'completed', progress: 100 },
19+
{ step: 'announcing-cids', status: 'completed', progress: 100 },
20+
{ step: 'finalizing-transaction', status: 'completed', progress: 100 },
21+
]
822

923
export default function Content() {
1024
const [uploadedFile, setUploadedFile] = useState<{ file: File; cid: string } | null>(null)
1125
const [isExpanded, setIsExpanded] = useState(true)
26+
const [expandedHistoryItems, setExpandedHistoryItems] = useState<Set<string>>(new Set())
27+
const [dragDropKey, setDragDropKey] = useState(0) // Key to force DragNDrop remount
1228
const { uploadState, uploadFile, resetUpload } = useFilecoinUpload()
29+
const { pieces: uploadHistory, refreshPieces, isLoading: isLoadingPieces } = useDatasetPieces()
30+
const context = useContext(FilecoinPinContext)
31+
if (!context) {
32+
throw new Error('Content must be used within FilecoinPinProvider')
33+
}
34+
35+
const { providerInfo, wallet, synapse } = context
36+
37+
// Determine if we're still initializing (wallet, synapse, provider)
38+
// Note: We don't block on isLoadingPieces - users can upload while history loads
39+
const isInitializing = wallet.status === 'loading' || wallet.status === 'idle'
40+
41+
// Get loading message based on current state
42+
const getLoadingMessage = () => {
43+
if (wallet.status === 'loading' || wallet.status === 'idle') {
44+
return 'Connecting to Filecoin network...'
45+
}
46+
if (!synapse) {
47+
return 'Initializing storage service...'
48+
}
49+
if (!providerInfo) {
50+
return 'Selecting storage provider...'
51+
}
52+
return 'Preparing upload interface...'
53+
}
54+
55+
// If wallet failed to load, show error instead of spinner
56+
if (wallet.status === 'error') {
57+
return (
58+
<div className="content">
59+
<PageTitle />
60+
<div className="error-message">
61+
<p>Failed to connect to Filecoin network: {wallet.error}</p>
62+
</div>
63+
</div>
64+
)
65+
}
1366

1467
const handleUpload = (file: File) => {
1568
// Set uploadedFile immediately to switch to progress view
@@ -27,61 +80,98 @@ export default function Content() {
2780
})
2881
}
2982

30-
const formatFileSize = (bytes: number): string => {
31-
if (bytes === 0) return '0 Bytes'
32-
const k = 1024
33-
const sizes = ['Bytes', 'KB', 'MB', 'GB']
34-
const i = Math.floor(Math.log(bytes) / Math.log(k))
35-
return `${parseFloat((bytes / k ** i).toFixed(2))} ${sizes[i]}`
36-
}
83+
// Refresh pieces list when upload completes
84+
useEffect(() => {
85+
const isUploadComplete = !uploadState.isUploading && uploadState.progress.every((p) => p.status === 'completed')
86+
if (isUploadComplete && uploadState.currentCid) {
87+
console.debug('[Content] Upload completed, refreshing pieces list')
88+
// Add a small delay to ensure the piece is indexed
89+
setTimeout(() => {
90+
refreshPieces()
91+
// Clear the uploadedFile after adding to history so the upload form shows again
92+
setUploadedFile(null)
93+
resetUpload()
94+
// Increment key to force DragNDrop to remount and clear its state
95+
setDragDropKey((prev) => prev + 1)
96+
}, 2000)
97+
}
98+
}, [uploadState.isUploading, uploadState.progress, uploadState.currentCid, refreshPieces, resetUpload])
99+
100+
// Auto-clear upload state on error
101+
useEffect(() => {
102+
if (uploadState.error) {
103+
// Keep showing the error for a bit, then auto-clear after 5 seconds
104+
const timer = setTimeout(() => {
105+
setUploadedFile(null)
106+
resetUpload()
107+
// Increment key to force DragNDrop to remount and clear its state
108+
setDragDropKey((prev) => prev + 1)
109+
}, 5000)
110+
return () => clearTimeout(timer)
111+
}
112+
}, [uploadState.error, resetUpload])
37113

38114
return (
39115
<div className="space-y-10">
40116
<PageTitle />
41117

42-
{uploadedFile ? (
118+
{/* Show drag-n-drop - disabled when actively uploading */}
119+
<div className="space-y-6">
120+
<Heading tag="h2">Upload a file</Heading>
121+
<DragNDrop isUploading={uploadState.isUploading} key={dragDropKey} onUpload={handleUpload} />
122+
</div>
123+
124+
{/* Show active upload progress */}
125+
{uploadedFile && (
43126
<div className="space-y-6">
44-
<Heading tag="h2">Uploaded files</Heading>
127+
<Heading tag="h2">Current upload</Heading>
45128
<UploadProgress
129+
cid={uploadState.currentCid}
46130
fileName={uploadedFile.file.name}
47131
fileSize={formatFileSize(uploadedFile.file.size)}
48132
isExpanded={isExpanded}
133+
network={wallet.status === 'ready' ? wallet.data.network : undefined}
49134
onToggleExpanded={() => setIsExpanded(!isExpanded)}
135+
pieceCid={uploadState.pieceCid}
50136
progress={uploadState.progress}
137+
providerName={providerInfo?.name || (providerInfo?.id ? String(providerInfo.id) : undefined)}
138+
transactionHash={uploadState.transactionHash}
51139
/>
52-
{uploadState.error && (
53-
<div className="error-message">
54-
<p>Upload failed: {uploadState.error}</p>
55-
<button
56-
onClick={() => {
57-
setUploadedFile(null)
58-
resetUpload()
59-
}}
60-
type="button"
61-
>
62-
Try Again
63-
</button>
64-
</div>
65-
)}
66-
{!uploadState.isUploading && uploadState.progress.every((p) => p.status === 'completed') && (
67-
<div className="success-message">
68-
<p>✅ File successfully uploaded! CID: {uploadedFile.cid}</p>
69-
<button
70-
onClick={() => {
71-
setUploadedFile(null)
72-
resetUpload()
73-
}}
74-
type="button"
75-
>
76-
Upload Another File
77-
</button>
78-
</div>
79-
)}
80140
</div>
81-
) : (
141+
)}
142+
143+
{(isLoadingPieces || isInitializing) && uploadHistory.length === 0 && (
144+
<LoadingState message={getLoadingMessage()} />
145+
)}
146+
{/* Always show upload history when available */}
147+
{uploadHistory.length > 0 && (
82148
<div className="space-y-6">
83-
<Heading tag="h2">Upload a file</Heading>
84-
<DragNDrop isUploading={uploadState.isUploading} onUpload={handleUpload} />
149+
<Heading tag="h2">Uploaded files</Heading>
150+
{uploadHistory.map((upload) => (
151+
<UploadProgress
152+
cid={upload.cid}
153+
fileName={upload.fileName}
154+
fileSize={upload.fileSize}
155+
isExpanded={expandedHistoryItems.has(upload.id)}
156+
key={upload.id}
157+
network={upload.network}
158+
onToggleExpanded={() => {
159+
setExpandedHistoryItems((prev) => {
160+
const next = new Set(prev)
161+
if (next.has(upload.id)) {
162+
next.delete(upload.id)
163+
} else {
164+
next.add(upload.id)
165+
}
166+
return next
167+
})
168+
}}
169+
pieceCid={upload.pieceCid}
170+
progress={COMPLETED_PROGRESS}
171+
providerName={upload.providerName}
172+
transactionHash={upload.transactionHash}
173+
/>
174+
))}
85175
</div>
86176
)}
87177
</div>

src/components/ui/badge-status.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import { CircleCheck, LoaderCircle } from 'lucide-react'
21
import { cva, type VariantProps } from 'class-variance-authority'
2+
import { CircleCheck, LoaderCircle } from 'lucide-react'
33
import { cn } from '../../utils/cn.ts'
44
import type { UploadProgress } from '../upload/upload-progress.tsx'
55

src/components/ui/card.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ type CardWrapperProps = {
88
type CardHeaderProps = {
99
title: string
1010
status: UploadProgress['status']
11-
estimatedTime?: number
11+
estimatedTime?: string
1212
}
1313

1414
type CardContentProps = {
@@ -23,7 +23,7 @@ function CardHeader({ title, status, estimatedTime }: CardHeaderProps) {
2323
return (
2424
<div className="flex items-center justify-between">
2525
<h3 className="font-medium">{title}</h3>
26-
{status === 'in-progress' && estimatedTime && <span className="text-sm text-zinc-400">{estimatedTime}%</span>}
26+
{status === 'in-progress' && estimatedTime && <span className="text-sm text-zinc-400">{estimatedTime}</span>}
2727
{status !== 'in-progress' && <BadgeStatus status={status} />}
2828
</div>
2929
)
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { Spinner } from './spinner.tsx'
2+
3+
interface LoadingStateProps {
4+
message: string
5+
className?: string
6+
}
7+
8+
export function LoadingState({ message, className = '' }: LoadingStateProps) {
9+
return (
10+
<div
11+
className={`flex flex-col items-center gap-4 p-8 bg-zinc-900 border border-zinc-700 rounded-lg mb-6 ${className}`}
12+
>
13+
<Spinner size="lg" />
14+
<p className="m-0 text-zinc-400 text-sm">{message}</p>
15+
</div>
16+
)
17+
}

src/components/ui/spinner.tsx

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
interface SpinnerProps {
2+
size?: 'sm' | 'md' | 'lg'
3+
className?: string
4+
}
5+
6+
const sizeMap = {
7+
sm: '1rem',
8+
md: '1.5rem',
9+
lg: '2rem',
10+
}
11+
12+
export function Spinner({ size = 'md', className = '' }: SpinnerProps) {
13+
const spinnerSize = sizeMap[size]
14+
15+
return (
16+
<output
17+
aria-label="Loading"
18+
aria-live="polite"
19+
className={`spinner ${className}`}
20+
style={{
21+
display: 'inline-block',
22+
width: spinnerSize,
23+
height: spinnerSize,
24+
border: '2px solid transparent',
25+
borderTop: '2px solid currentColor',
26+
borderRadius: '50%',
27+
animation: 'button-spin 1s linear infinite',
28+
}}
29+
/>
30+
)
31+
}

src/components/upload/drag-n-drop.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import { Root } from '@radix-ui/react-form'
2-
import { useContext, useState, useEffect } from 'react'
2+
import { useContext, useEffect, useState } from 'react'
3+
import { FilecoinPinContext } from '../../context/filecoin-pin-provider.tsx'
34
import { FilePicker } from '../file-picker/index.tsx'
45
import { ButtonBase as Button } from '../ui/button/button-base.tsx'
5-
import { FilecoinPinContext } from '../../context/filecoin-pin-provider.tsx'
66

77
interface DragNDropProps {
88
onFileSelected?: (file: File) => void

0 commit comments

Comments
 (0)