From 7064439815c95f733370ab9204244e5889b686dd Mon Sep 17 00:00:00 2001 From: Barbara Peric Date: Thu, 16 Oct 2025 12:21:49 +0100 Subject: [PATCH 1/3] feat: add 'published' status to badge and update upload progress logic --- src/components/ui/badge-status.tsx | 5 ++++- src/hooks/use-upload-progress.ts | 9 ++++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/src/components/ui/badge-status.tsx b/src/components/ui/badge-status.tsx index 06e1ee1..fe067d2 100644 --- a/src/components/ui/badge-status.tsx +++ b/src/components/ui/badge-status.tsx @@ -3,7 +3,7 @@ import { CircleCheck, LoaderCircle } from 'lucide-react' import type { Progress } from '../../types/upload-progress.ts' import { cn } from '../../utils/cn.ts' -export type Status = Progress['status'] | 'pinned' +export type Status = Progress['status'] | 'pinned' | 'published' type BadgeStatusProps = VariantProps & { status: Status @@ -15,6 +15,7 @@ const badgeVariants = cva('inline-flex items-center gap-1 pl-1.5 pr-2 py-0.5 rou 'in-progress': 'bg-badge-in-progress text-badge-in-progress-text border border-badge-in-progress-border', completed: 'bg-brand-950 text-brand-700 border border-brand-900', pinned: 'bg-brand-950 text-brand-700 border border-brand-900', + published: 'bg-yellow-600/30 border border-yellow-400/20 text-yellow-200', error: null, pending: 'bg-zinc-800 border border-zinc-700 text-zinc-300', }, @@ -28,6 +29,7 @@ const statusIcons: Record = { 'in-progress': , completed: , pinned: , + published: null, error: null, pending: null, } @@ -36,6 +38,7 @@ const statusLabels: Record = { 'in-progress': 'In progress', completed: 'Complete', pinned: 'Pinned', + published: 'Published', error: null, pending: 'Pending', } diff --git a/src/hooks/use-upload-progress.ts b/src/hooks/use-upload-progress.ts index 4d6341a..f4b4952 100644 --- a/src/hooks/use-upload-progress.ts +++ b/src/hooks/use-upload-progress.ts @@ -1,3 +1,4 @@ +import type { Status } from '@/components/ui/badge-status.tsx' import { useMemo } from 'react' import type { Progress } from '../types/upload-progress.ts' import { createStepGroup } from '../utils/upload-status.ts' @@ -32,7 +33,7 @@ export interface UploadProgressInfo { /** * Badge status for the file card header */ - badgeStatus: 'pinned' | 'error' | 'in-progress' | 'pending' + badgeStatus: Status } /** @@ -112,11 +113,17 @@ export function useUploadProgress(progresses: Progress[], cid?: string): UploadP // Check if any step is in error (excluding IPNI failures) const hasError = progresses.some((p) => p.status === 'error' && p.step !== 'announcing-cids') + const finalizingStep = progresses.find((p) => p.step === 'finalizing-transaction') + const announcingStep = progresses.find((p) => p.step === 'announcing-cids') + // Determine the badge status for the file card header const getBadgeStatus = (): UploadProgressInfo['badgeStatus'] => { if (isCompleted) return 'pinned' if (hasError) return 'error' + if (finalizingStep?.status === 'completed' && announcingStep?.status !== 'completed') { + return 'published' + } // Check if any step is in progress if (progresses.some((p) => p.status === 'in-progress')) return 'in-progress' // If no steps are in progress but not all completed, must be pending From 99405985a156d750de91ceef734a4211ff1a34ac Mon Sep 17 00:00:00 2001 From: Barbara Peric Date: Mon, 20 Oct 2025 16:45:21 +0200 Subject: [PATCH 2/3] refactor: replace Progress type with StepState for upload progress handling This update transitions the upload progress management from using the Progress type to the new StepState type, enhancing type safety and clarity. The changes include updates to various components and hooks to accommodate the new structure, ensuring consistent handling of upload steps and statuses throughout the application. --- src/components/layout/content.tsx | 8 +- .../upload/car-upload-and-ipni-card.tsx | 36 ++--- .../upload/progress-card-combined.tsx | 27 ++-- src/components/upload/progress-card.tsx | 20 +-- src/components/upload/upload-progress.tsx | 12 +- src/components/upload/upload-status.tsx | 14 +- src/constants/upload-status.tsx | 3 - src/hooks/use-upload-orchestration.ts | 6 +- src/hooks/use-upload-progress.ts | 140 ++++-------------- src/types/upload-progress.ts | 8 - src/types/upload/stage.ts | 14 ++ src/types/upload/step.ts | 17 +++ src/utils/upload-status.ts | 63 -------- src/utils/upload/stage.ts | 55 +++++++ src/utils/upload/step.ts | 31 ++++ src/utils/upload/upload.ts | 41 +++++ 16 files changed, 247 insertions(+), 248 deletions(-) delete mode 100644 src/constants/upload-status.tsx delete mode 100644 src/types/upload-progress.ts create mode 100644 src/types/upload/stage.ts create mode 100644 src/types/upload/step.ts delete mode 100644 src/utils/upload-status.ts create mode 100644 src/utils/upload/stage.ts create mode 100644 src/utils/upload/step.ts create mode 100644 src/utils/upload/upload.ts diff --git a/src/components/layout/content.tsx b/src/components/layout/content.tsx index c2023df..127e0e2 100644 --- a/src/components/layout/content.tsx +++ b/src/components/layout/content.tsx @@ -1,11 +1,11 @@ import { useState } from 'react' import { Alert } from '@/components/ui/alert.tsx' -import type { Progress } from '@/types/upload-progress.ts' import { useUploadHistory } from '../../context/upload-history-context.tsx' import { useFilecoinPinContext } from '../../hooks/use-filecoin-pin-context.ts' import { useUploadExpansion } from '../../hooks/use-upload-expansion.ts' import { useUploadOrchestration } from '../../hooks/use-upload-orchestration.ts' import { useUploadUI } from '../../hooks/use-upload-ui.ts' +import type { StepState } from '../../types/upload/step.ts' import { formatFileSize } from '../../utils/format-file-size.ts' import { Heading } from '../ui/heading.tsx' import { LoadingState } from '../ui/loading-state.tsx' @@ -14,7 +14,7 @@ import DragNDrop from '../upload/drag-n-drop.tsx' import { UploadStatus } from '../upload/upload-status.tsx' // Completed state for displaying upload history -const COMPLETED_PROGRESS: Progress[] = [ +const COMPLETED_PROGRESS: StepState[] = [ { step: 'creating-car', status: 'completed', progress: 100 }, { step: 'checking-readiness', status: 'completed', progress: 100 }, { step: 'uploading-car', status: 'completed', progress: 100 }, @@ -94,7 +94,7 @@ export default function Content() { isExpanded={activeUploadExpanded} onToggleExpanded={() => setActiveUploadExpanded(!activeUploadExpanded)} pieceCid={activeUpload.pieceCid ?? ''} - progresses={activeUpload.progress} + stepStates={activeUpload.progress} transactionHash={activeUpload.transactionHash ?? ''} /> @@ -119,7 +119,7 @@ export default function Content() { key={upload.id} onToggleExpanded={() => toggleExpansion(upload.id)} pieceCid={upload.pieceCid} - progresses={COMPLETED_PROGRESS} + stepStates={COMPLETED_PROGRESS} transactionHash={upload.transactionHash} /> ))} diff --git a/src/components/upload/car-upload-and-ipni-card.tsx b/src/components/upload/car-upload-and-ipni-card.tsx index 18f1889..136585c 100644 --- a/src/components/upload/car-upload-and-ipni-card.tsx +++ b/src/components/upload/car-upload-and-ipni-card.tsx @@ -1,8 +1,8 @@ import { getIpfsGatewayDownloadLink, getIpfsGatewayRenderLink } from '@/utils/links.ts' -import { COMBINED_STEPS } from '../../constants/upload-status.tsx' import { INPI_ERROR_MESSAGE } from '../../hooks/use-filecoin-upload.ts' import { useUploadProgress } from '../../hooks/use-upload-progress.ts' -import type { Progress } from '../../types/upload-progress.ts' +import { STAGE_STEPS } from '../../types/upload/stage.ts' +import type { StepState } from '../../types/upload/step.ts' import { Alert } from '../ui/alert.tsx' import { Card } from '../ui/card.tsx' import { DownloadButton } from '../ui/download-button.tsx' @@ -11,7 +11,7 @@ import { ProgressCard } from './progress-card.tsx' import { ProgressCardCombined } from './progress-card-combined.tsx' interface CarUploadAndIpniCardProps { - progresses: Progress[] + stepStates: StepState[] cid?: string fileName: string } @@ -22,14 +22,14 @@ interface CarUploadAndIpniCardProps { * It will shrink to a single card if the uploading-car and announcing-cids steps are completed. * Otherwise, it will display the progress of the uploading-car and announcing-cids steps. */ -export const CarUploadAndIpniCard = ({ progresses, cid, fileName }: CarUploadAndIpniCardProps) => { +export const CarUploadAndIpniCard = ({ stepStates, cid, fileName }: CarUploadAndIpniCardProps) => { // Use the upload progress hook to calculate all progress-related values - const { getCombinedFirstStageProgress, getCombinedFirstStageStatus, hasIpniFailure } = useUploadProgress( - progresses, - cid - ) - const uploadingStep = progresses.find((p) => p.step === 'uploading-car') - const announcingStep = progresses.find((p) => p.step === 'announcing-cids') + const { firstStageProgress, firstStageStatus, hasUploadIpniFailure } = useUploadProgress({ + stepStates, + cid, + }) + const uploadingStep = stepStates.find((stepState) => stepState.step === 'uploading-car') + const announcingStep = stepStates.find((stepState) => stepState.step === 'announcing-cids') const shouldShowCidCard = uploadingStep?.status === 'completed' && @@ -39,33 +39,33 @@ export const CarUploadAndIpniCard = ({ progresses, cid, fileName }: CarUploadAnd if (shouldShowCidCard) { return ( - {hasIpniFailure && } + {hasUploadIpniFailure && } } title="IPFS Root CID" > - {!hasIpniFailure && } + {!hasUploadIpniFailure && } ) } // Filter progresses to only include the combined steps for ProgressCardCombined - const combinedProgresses = progresses.filter((p) => COMBINED_STEPS.includes(p.step)) + const firstStageStates = stepStates.filter((stepState) => STAGE_STEPS['first-stage'].includes(stepState.step)) return ( <> - {announcingStep && } + {announcingStep && } ) } diff --git a/src/components/upload/progress-card-combined.tsx b/src/components/upload/progress-card-combined.tsx index 1375126..816d0e1 100644 --- a/src/components/upload/progress-card-combined.tsx +++ b/src/components/upload/progress-card-combined.tsx @@ -1,34 +1,27 @@ -import type { Progress } from '../../types/upload-progress.ts' -import { getEstimatedTime, getStepLabel } from '../../utils/upload-status.ts' +import type { StepState } from '../../types/upload/step.ts' +import { getStepEstimatedTime, getStepLabel } from '../../utils/upload/step.ts' import { Card } from '../ui/card.tsx' import { ProgressBar } from '../ui/progress-bar.tsx' interface ProgressCardCombinedProps { - progresses: Array - getCombinedFirstStageStatus: () => Progress['status'] - getCombinedFirstStageProgress: () => number + stepStates: Array + firstStageStatus: StepState['status'] + firstStageProgress: StepState['progress'] } -function ProgressCardCombined({ - progresses, - getCombinedFirstStageStatus, - getCombinedFirstStageProgress, -}: ProgressCardCombinedProps) { - const hasCreatingCarStep = progresses.find((progress) => progress.step === 'creating-car') - - const firstStagestatus = getCombinedFirstStageStatus() - const firstStageProgress = getCombinedFirstStageProgress() +function ProgressCardCombined({ stepStates, firstStageStatus, firstStageProgress }: ProgressCardCombinedProps) { + const hasCreatingCarStep = stepStates.find((stepState) => stepState.step === 'creating-car') if (!hasCreatingCarStep) return null return ( - {firstStagestatus === 'in-progress' && } + {firstStageStatus === 'in-progress' && } ) } diff --git a/src/components/upload/progress-card.tsx b/src/components/upload/progress-card.tsx index b611ed7..2490699 100644 --- a/src/components/upload/progress-card.tsx +++ b/src/components/upload/progress-card.tsx @@ -1,29 +1,29 @@ -import type { Progress } from '@/types/upload-progress.ts' -import { getEstimatedTime, getStepLabel } from '../../utils/upload-status.ts' +import type { StepState } from '../../types/upload/step.ts' +import { getStepEstimatedTime, getStepLabel } from '../../utils/upload/step.ts' import { Alert } from '../ui/alert.tsx' import { Card } from '../ui/card.tsx' import { TextWithCopyToClipboard } from '../ui/text-with-copy-to-clipboard.tsx' interface ProgressCardProps { - progress: Progress + stepState: StepState transactionHash?: string } -function ProgressCard({ progress, transactionHash }: ProgressCardProps) { +function ProgressCard({ stepState, transactionHash }: ProgressCardProps) { return ( - {progress.error && ( - + {stepState.error && ( + )} - {progress.step === 'finalizing-transaction' && transactionHash && ( + {stepState.step === 'finalizing-transaction' && transactionHash && ( + stepStates: Array transactionHash?: UploadStatusProps['transactionHash'] cid?: string fileName: string } -function UploadProgress({ progresses, transactionHash, cid, fileName }: UploadProgressProps) { - const finalizingStep = progresses.find((p) => p.step === 'finalizing-transaction') +function UploadProgress({ stepStates, transactionHash, cid, fileName }: UploadProgressProps) { + const finalizingStep = stepStates.find((stepState) => stepState.step === 'finalizing-transaction') return ( <> - - {finalizingStep && } + + {finalizingStep && } ) } diff --git a/src/components/upload/upload-status.tsx b/src/components/upload/upload-status.tsx index c79ff9f..d3d9afc 100644 --- a/src/components/upload/upload-status.tsx +++ b/src/components/upload/upload-status.tsx @@ -1,6 +1,6 @@ import type { DatasetPiece } from '../../hooks/use-dataset-pieces.ts' import { useUploadProgress } from '../../hooks/use-upload-progress.ts' -import type { Progress } from '../../types/upload-progress.ts' +import type { StepState } from '../../types/upload/step.ts' import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from '../ui/accordion.tsx' import { FileInfo } from '../ui/file-info.tsx' import { UploadCompleted } from './upload-completed.tsx' @@ -9,7 +9,7 @@ import { UploadProgress } from './upload-progress.tsx' export interface UploadStatusProps { fileName: DatasetPiece['fileName'] fileSize: DatasetPiece['fileSize'] - progresses: Array + stepStates: Array isExpanded?: boolean onToggleExpanded?: () => void cid?: DatasetPiece['cid'] @@ -22,7 +22,7 @@ export interface UploadStatusProps { function UploadStatus({ fileName, fileSize, - progresses, + stepStates, isExpanded = true, onToggleExpanded, cid, @@ -31,7 +31,7 @@ function UploadStatus({ transactionHash, }: UploadStatusProps) { // Use the upload progress hook to calculate all progress-related values - const { isCompleted, badgeStatus } = useUploadProgress(progresses, cid) + const { isUploadSuccessful, uploadBadgeStatus } = useUploadProgress({ stepStates, cid }) return ( - + - {isCompleted && cid ? ( + {isUploadSuccessful && cid ? ( ) : ( - + )} diff --git a/src/constants/upload-status.tsx b/src/constants/upload-status.tsx deleted file mode 100644 index 899043f..0000000 --- a/src/constants/upload-status.tsx +++ /dev/null @@ -1,3 +0,0 @@ -import type { StepType } from '../types/upload-progress.ts' - -export const COMBINED_STEPS: StepType[] = ['creating-car', 'checking-readiness', 'uploading-car'] as const diff --git a/src/hooks/use-upload-orchestration.ts b/src/hooks/use-upload-orchestration.ts index 9e7d1b9..78531f8 100644 --- a/src/hooks/use-upload-orchestration.ts +++ b/src/hooks/use-upload-orchestration.ts @@ -52,7 +52,7 @@ export function useUploadOrchestration() { const { uploadState, uploadFile, resetUpload } = useFilecoinUpload() const { addUpload } = useUploadHistory() const { storageContext, providerInfo, wallet } = useFilecoinPinContext() - const { isCompleted } = useUploadProgress(uploadState.progress, uploadState.currentCid) + const { isUploadSuccessful } = useUploadProgress({ stepStates: uploadState.progress, cid: uploadState.currentCid }) // Track the file being uploaded (for displaying metadata like fileName) const [uploadedFile, setUploadedFile] = useState(null) @@ -71,7 +71,7 @@ export function useUploadOrchestration() { * 4. Force DragNDrop remount to clear its state */ useEffect(() => { - if (isCompleted && uploadState.pieceCid && uploadedFile && storageContext && providerInfo) { + if (isUploadSuccessful && uploadState.pieceCid && uploadedFile && storageContext && providerInfo) { console.debug('[UploadOrchestration] Upload completed, adding to history') // Mark this piece CID to be auto-expanded when it appears in history @@ -104,7 +104,7 @@ export function useUploadOrchestration() { setDragDropKey((prev) => prev + 1) } }, [ - isCompleted, + isUploadSuccessful, uploadState.pieceCid, uploadState.currentCid, uploadState.transactionHash, diff --git a/src/hooks/use-upload-progress.ts b/src/hooks/use-upload-progress.ts index f4b4952..a3c5414 100644 --- a/src/hooks/use-upload-progress.ts +++ b/src/hooks/use-upload-progress.ts @@ -1,39 +1,21 @@ -import type { Status } from '@/components/ui/badge-status.tsx' import { useMemo } from 'react' -import type { Progress } from '../types/upload-progress.ts' -import { createStepGroup } from '../utils/upload-status.ts' +import type { Status } from '@/components/ui/badge-status.tsx' +import type { StepState } from '../types/upload/step.ts' +import { getFirstStageProgress, getFirstStageStatus } from '../utils/upload/stage.ts' +import { getUploadBadgeStatus, getUploadOutcome } from '../utils/upload/upload.ts' export interface UploadProgressInfo { - /** - * Get the combined progress percentage for the first stage - * (creating-car + checking-readiness + uploading-car) - */ - getCombinedFirstStageProgress: () => number - - /** - * Get the combined status for the first stage - */ - getCombinedFirstStageStatus: () => Progress['status'] - - /** - * True if the IPNI announcement step failed - */ - hasIpniFailure: boolean - - /** - * True if all steps are completed (treating IPNI failures as acceptable) - */ - isCompleted: boolean - - /** - * True if any step has an error (excluding IPNI failures which are acceptable) - */ - hasError: boolean + firstStageProgress: StepState['progress'] + firstStageStatus: StepState['status'] + hasUploadIpniFailure: boolean + isUploadSuccessful: boolean + isUploadFailure: boolean + uploadBadgeStatus: Status +} - /** - * Badge status for the file card header - */ - badgeStatus: Status +type useUploadProgressProps = { + stepStates: StepState[] + cid?: string } /** @@ -49,7 +31,7 @@ export interface UploadProgressInfo { * @example * ```tsx * function UploadCard({ progresses, cid }) { - * const { isCompleted, badgeStatus, hasIpniFailure } = useUploadProgress(progresses, cid) + * const { isCompleted, badgeStatus, hasUploadIpniFailure } = useUploadProgress(progresses, cid) * * return ( * @@ -59,84 +41,24 @@ export interface UploadProgressInfo { * } * ``` */ -export function useUploadProgress(progresses: Progress[], cid?: string): UploadProgressInfo { +export function useUploadProgress({ stepStates, cid }: useUploadProgressProps): UploadProgressInfo { return useMemo(() => { - // Calculate combined progress for the first stage (creating CAR + checking readiness + uploading) - const getCombinedFirstStageProgress = () => { - const { creatingCar, checkingReadiness, uploadingCar } = createStepGroup(progresses) - const total = creatingCar.progress + checkingReadiness.progress + uploadingCar.progress - return Math.round(total / 3) - } - - // Get the status for the combined first stage - const getCombinedFirstStageStatus = (): Progress['status'] => { - const { creatingCar, checkingReadiness, uploadingCar } = createStepGroup(progresses) - - // If any stage has error, show error - if ( - uploadingCar?.status === 'error' || - checkingReadiness?.status === 'error' || - creatingCar?.status === 'error' - ) { - return 'error' - } - - // If uploading-car is completed, the whole stage is completed - if (uploadingCar?.status === 'completed') { - return 'completed' - } - - // If any stage is in progress OR completed (but not all completed), show in-progress - const startedStages: Progress['status'][] = ['in-progress', 'completed'] - const hasStarted = - (uploadingCar?.status && startedStages.includes(uploadingCar.status)) || - (checkingReadiness?.status && startedStages.includes(checkingReadiness.status)) || - (creatingCar?.status && startedStages.includes(creatingCar.status)) - - if (hasStarted) { - return 'in-progress' - } - - // Otherwise pending - return 'pending' - } - - const hasIpniFailure = progresses.find((p) => p.step === 'announcing-cids')?.status === 'error' - - // Check if all steps are completed AND we have a CID (upload actually finished) - // BUT treat IPNI failures as still "completed" since file is stored on Filecoin - const isCompleted = - Boolean(cid) && - progresses.every((p) => { - return p.status === 'completed' || (p.step === 'announcing-cids' && p.status === 'error') - }) - - // Check if any step is in error (excluding IPNI failures) - const hasError = progresses.some((p) => p.status === 'error' && p.step !== 'announcing-cids') - const finalizingStep = progresses.find((p) => p.step === 'finalizing-transaction') - const announcingStep = progresses.find((p) => p.step === 'announcing-cids') - - - // Determine the badge status for the file card header - const getBadgeStatus = (): UploadProgressInfo['badgeStatus'] => { - if (isCompleted) return 'pinned' - if (hasError) return 'error' - if (finalizingStep?.status === 'completed' && announcingStep?.status !== 'completed') { - return 'published' - } - // Check if any step is in progress - if (progresses.some((p) => p.status === 'in-progress')) return 'in-progress' - // If no steps are in progress but not all completed, must be pending - return 'pending' - } + const firstStageProgress = getFirstStageProgress(stepStates) + const firstStageStatus = getFirstStageStatus(stepStates) + const { hasUploadIpniFailure, isUploadSuccessful, isUploadFailure } = getUploadOutcome({ stepStates, cid }) + const uploadBadgeStatus = getUploadBadgeStatus({ + isUploadSuccessful, + isUploadFailure, + stepStates, + }) return { - getCombinedFirstStageProgress, - getCombinedFirstStageStatus, - hasIpniFailure, - isCompleted, - hasError, - badgeStatus: getBadgeStatus(), + firstStageProgress, + firstStageStatus, + hasUploadIpniFailure, + isUploadSuccessful, + isUploadFailure, + uploadBadgeStatus, } - }, [progresses, cid]) + }, [stepStates, cid]) } diff --git a/src/types/upload-progress.ts b/src/types/upload-progress.ts deleted file mode 100644 index e1cac31..0000000 --- a/src/types/upload-progress.ts +++ /dev/null @@ -1,8 +0,0 @@ -export interface Progress { - step: 'creating-car' | 'uploading-car' | 'checking-readiness' | 'announcing-cids' | 'finalizing-transaction' - progress: number // 0-100 - status: 'pending' | 'in-progress' | 'completed' | 'error' - error?: string -} - -export type StepType = Progress['step'] diff --git a/src/types/upload/stage.ts b/src/types/upload/stage.ts new file mode 100644 index 0000000..4fe8464 --- /dev/null +++ b/src/types/upload/stage.ts @@ -0,0 +1,14 @@ +import type { StepState } from '../upload/step.ts' + +export type StageId = 'first-stage' | 'second-stage' | 'third-stage' + +export const STAGE_STEPS: Record = { + 'first-stage': ['creating-car', 'checking-readiness', 'uploading-car'], + 'second-stage': ['announcing-cids'], + 'third-stage': ['finalizing-transaction'], +} as const + +export type FirstStageGroup = Record< + 'creatingCar' | 'checkingReadiness' | 'uploadingCar', + { progress: number; status: StepState['status'] } +> diff --git a/src/types/upload/step.ts b/src/types/upload/step.ts new file mode 100644 index 0000000..4684cfd --- /dev/null +++ b/src/types/upload/step.ts @@ -0,0 +1,17 @@ +export type StepName = + | 'creating-car' + | 'checking-readiness' + | 'uploading-car' + | 'announcing-cids' + | 'finalizing-transaction' + +export type StepStatus = 'pending' | 'in-progress' | 'completed' | 'error' + +export interface StepState { + step: StepName + progress: number // 0–100 + status: StepStatus + error?: string +} + +export type StepType = StepState['step'] diff --git a/src/utils/upload-status.ts b/src/utils/upload-status.ts deleted file mode 100644 index 8a8a7e8..0000000 --- a/src/utils/upload-status.ts +++ /dev/null @@ -1,63 +0,0 @@ -import type { Progress } from '@/types/upload-progress.ts' - -type FirstSteoGroupRecord = Record< - 'creatingCar' | 'checkingReadiness' | 'uploadingCar', - { progress: number; status: Progress['status'] } -> - -// simple type to help with searching for UploadProgress['step'] in the first step group -export const firstStepGroup: FirstSteoGroupRecord = { - creatingCar: { progress: 0, status: 'pending' }, - checkingReadiness: { progress: 0, status: 'pending' }, - uploadingCar: { progress: 0, status: 'pending' }, -} - -export function createStepGroup(progress: Progress[]) { - // Map kebab-case step names to camelCase keys - const stepMap: Record = { - 'creating-car': 'creatingCar', - 'checking-readiness': 'checkingReadiness', - 'uploading-car': 'uploadingCar', - } - - return progress.reduce( - (acc, p) => { - const key = stepMap[p.step] - if (key) { - acc[key] = { progress: p.progress, status: p.status } - } - return acc - }, - { - creatingCar: { progress: 0, status: 'pending' }, - checkingReadiness: { progress: 0, status: 'pending' }, - uploadingCar: { progress: 0, status: 'pending' }, - } - ) -} - -export function getStepLabel(step: Progress['step']) { - switch (step) { - case 'creating-car': - case 'checking-readiness': - case 'uploading-car': - return 'Preparing service, creating CAR file, and uploading to the Filecoin SP' - case 'announcing-cids': - return 'Announcing IPFS CIDs to IPNI' - case 'finalizing-transaction': - return 'Finalizing storage transaction on Calibration testnet' - } -} - -export function getEstimatedTime(step: Progress['step']) { - switch (step) { - case 'creating-car': - case 'checking-readiness': - case 'uploading-car': - return 'Estimated time: ~30 seconds' - case 'announcing-cids': - return 'Estimated time: ~30 seconds' - case 'finalizing-transaction': - return 'Estimated time: ~30-60 seconds' - } -} diff --git a/src/utils/upload/stage.ts b/src/utils/upload/stage.ts new file mode 100644 index 0000000..0b42379 --- /dev/null +++ b/src/utils/upload/stage.ts @@ -0,0 +1,55 @@ +import type { FirstStageGroup } from '../../types/upload/stage.ts' +import type { StepState } from '../../types/upload/step.ts' +import { stepHasActiveStatus } from './step.ts' + +export const firstStageGroup: FirstStageGroup = { + creatingCar: { progress: 0, status: 'pending' }, + checkingReadiness: { progress: 0, status: 'pending' }, + uploadingCar: { progress: 0, status: 'pending' }, +} + +function getFirstStageState(stepStates: StepState[]) { + const stepMap: Record = { + 'creating-car': 'creatingCar', + 'checking-readiness': 'checkingReadiness', + 'uploading-car': 'uploadingCar', + } + + return stepStates.reduce( + (acc, stepState) => { + const key = stepMap[stepState.step] + if (key) { + acc[key] = { progress: stepState.progress, status: stepState.status } + } + return acc + }, + { + creatingCar: { progress: 0, status: 'pending' }, + checkingReadiness: { progress: 0, status: 'pending' }, + uploadingCar: { progress: 0, status: 'pending' }, + } + ) +} + +// Calculate combined progress for the first stage (creating CAR + checking readiness + uploading) +export function getFirstStageProgress(stepStates: StepState[]) { + const { creatingCar, checkingReadiness, uploadingCar } = getFirstStageState(stepStates) + const total = creatingCar.progress + checkingReadiness.progress + uploadingCar.progress + return Math.round(total / 3) +} + +export function getFirstStageStatus(stepStates: StepState[]) { + const firstStageState = getFirstStageState(stepStates) + + const { creatingCar, checkingReadiness, uploadingCar } = firstStageState + + if (uploadingCar?.status === 'error' || checkingReadiness?.status === 'error' || creatingCar?.status === 'error') { + return 'error' + } + + if (uploadingCar?.status === 'completed') return 'completed' + + if (Object.values(firstStageState).some((s) => stepHasActiveStatus(s.status))) return 'in-progress' + + return 'pending' +} diff --git a/src/utils/upload/step.ts b/src/utils/upload/step.ts new file mode 100644 index 0000000..6efe87a --- /dev/null +++ b/src/utils/upload/step.ts @@ -0,0 +1,31 @@ +import type { StepState } from '../../types/upload/step.ts' + +export function getStepLabel(step: StepState['step']) { + switch (step) { + case 'creating-car': + case 'checking-readiness': + case 'uploading-car': + return 'Preparing service, creating CAR file, and uploading to the Filecoin SP' + case 'announcing-cids': + return 'Announcing IPFS CIDs to IPNI' + case 'finalizing-transaction': + return 'Finalizing storage transaction on Calibration testnet' + } +} + +export function getStepEstimatedTime(step: StepState['step']) { + switch (step) { + case 'creating-car': + case 'checking-readiness': + case 'uploading-car': + return 'Estimated time: ~30 seconds' + case 'announcing-cids': + return 'Estimated time: ~30 seconds' + case 'finalizing-transaction': + return 'Estimated time: ~30-60 seconds' + } +} + +export function stepHasActiveStatus(status?: StepState['status']) { + return status === 'completed' || status === 'in-progress' +} diff --git a/src/utils/upload/upload.ts b/src/utils/upload/upload.ts new file mode 100644 index 0000000..3de6835 --- /dev/null +++ b/src/utils/upload/upload.ts @@ -0,0 +1,41 @@ +import type { StepState } from '../../types/upload/step.ts' + +type getUploadOutcomeProps = { + stepStates: StepState[] + cid?: string +} + +export function getUploadOutcome({ stepStates, cid }: getUploadOutcomeProps) { + const hasUploadIpniFailure = stepStates.find((stepState) => stepState.step === 'announcing-cids')?.status === 'error' + + const isUploadFailure = stepStates.some( + (stepState) => stepState.status === 'error' && stepState.step !== 'announcing-cids' + ) + + const isUploadSuccessful = + Boolean(cid) && + stepStates.every((stepState) => { + return stepState.status === 'completed' || (stepState.step === 'announcing-cids' && stepState.status === 'error') + }) + + return { hasUploadIpniFailure, isUploadSuccessful, isUploadFailure } +} + +type getUploadBadgeStatusProps = { + isUploadSuccessful: boolean + isUploadFailure: boolean + stepStates: StepState[] +} + +export function getUploadBadgeStatus({ isUploadSuccessful, isUploadFailure, stepStates }: getUploadBadgeStatusProps) { + const finalizingTransactionStep = stepStates.find((stepState) => stepState.step === 'finalizing-transaction') + const announcingCidsStep = stepStates.find((stepState) => stepState.step === 'announcing-cids') + + if (isUploadSuccessful) return 'pinned' + if (isUploadFailure) return 'error' + if (finalizingTransactionStep?.status === 'completed' && announcingCidsStep?.status !== 'completed') { + return 'published' + } + if (stepStates.some((stepState) => stepState.status === 'in-progress')) return 'in-progress' + return 'pending' +} From 51c6f9932c611ac9f652303c40434d3789d5c8cb Mon Sep 17 00:00:00 2001 From: Barbara Peric Date: Tue, 21 Oct 2025 09:31:38 +0200 Subject: [PATCH 3/3] Fix imports --- src/components/ui/badge-status.tsx | 4 ++-- src/components/ui/card.tsx | 4 ++-- src/hooks/use-filecoin-upload.ts | 8 ++++---- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/components/ui/badge-status.tsx b/src/components/ui/badge-status.tsx index fe067d2..8bff7f8 100644 --- a/src/components/ui/badge-status.tsx +++ b/src/components/ui/badge-status.tsx @@ -1,9 +1,9 @@ import { cva, type VariantProps } from 'class-variance-authority' import { CircleCheck, LoaderCircle } from 'lucide-react' -import type { Progress } from '../../types/upload-progress.ts' +import type { StepState } from '../../types/upload/step.ts' import { cn } from '../../utils/cn.ts' -export type Status = Progress['status'] | 'pinned' | 'published' +export type Status = StepState['status'] | 'pinned' | 'published' type BadgeStatusProps = VariantProps & { status: Status diff --git a/src/components/ui/card.tsx b/src/components/ui/card.tsx index 6190048..955c395 100644 --- a/src/components/ui/card.tsx +++ b/src/components/ui/card.tsx @@ -1,4 +1,4 @@ -import type { Progress } from '../../types/upload-progress.ts' +import type { StepState } from '../../types/upload/step.ts' import { BadgeStatus } from './badge-status.tsx' import { Heading } from './heading.tsx' import { Spinner } from './spinner.tsx' @@ -9,7 +9,7 @@ type CardWrapperProps = { type CardHeaderProps = { title: string - status: Progress['status'] + status: StepState['status'] estimatedTime?: string withSpinner?: boolean } diff --git a/src/hooks/use-filecoin-upload.ts b/src/hooks/use-filecoin-upload.ts index 8567669..c26c1fd 100644 --- a/src/hooks/use-filecoin-upload.ts +++ b/src/hooks/use-filecoin-upload.ts @@ -2,21 +2,21 @@ import { createCarFromFile } from 'filecoin-pin/core/unixfs' import { checkUploadReadiness, executeUpload } from 'filecoin-pin/core/upload' import pino from 'pino' import { useCallback, useMemo, useState } from 'react' -import type { Progress } from '../types/upload-progress.ts' +import type { StepState } from '../types/upload/step.ts' import { formatFileSize } from '../utils/format-file-size.ts' import { useFilecoinPinContext } from './use-filecoin-pin-context.ts' import { useIpniCheck } from './use-ipni-check.ts' interface UploadState { isUploading: boolean - progress: Progress[] + progress: StepState[] error?: string currentCid?: string pieceCid?: string transactionHash?: string } -const initialProgress: Progress[] = [ +const initialProgress: StepState[] = [ { step: 'creating-car', progress: 0, status: 'pending' }, { step: 'checking-readiness', progress: 0, status: 'pending' }, { step: 'uploading-car', progress: 0, status: 'pending' }, @@ -58,7 +58,7 @@ export const useFilecoinUpload = () => { progress: initialProgress, }) - const updateProgress = useCallback((step: Progress['step'], updates: Partial) => { + const updateProgress = useCallback((step: StepState['step'], updates: Partial) => { setUploadState((prev) => ({ ...prev, progress: prev.progress.map((p) => (p.step === step ? { ...p, ...updates } : p)),