import { forwardRef, ForwardedRef, useEffect, useState } from 'react'
import { PktButton, PktIcon, PktProgressbar } from '..'
import { PktModal } from '../modal/Modal'
import { Truncate } from './Truncate'
import { TFileAndTransfer, TItemRenderer, TQueueItemOperation, TTransferItemInProgress } from './types'
import { formatFileSize } from './utils'
const CancelTransferButton = (props: { onClick: () => void; label?: string; ariaLabel?: string }) => (
)
const LoadingText = () => {
const [dotCount, setDotCount] = useState(1)
useEffect(() => {
const interval = setInterval(() => {
setDotCount((prev) => (prev >= 3 ? 1 : prev + 1))
}, 400)
return () => clearInterval(interval)
}, [])
return (
Laster opp{'.'.repeat(dotCount)}
)
}
export const TransferInProgress = ({
cancelTransfer,
transferItem,
}: {
transferItem: TTransferItemInProgress
cancelTransfer: () => void
}) => {
const showProgressBar = transferItem.showProgress ?? transferItem.progress !== 0
return (
<>
{transferItem.attributes['targetFilename']}
{showProgressBar ? (
<>
{formatFileSize(transferItem.file.size)}
{`${Math.round(transferItem.progress * 100)}%`}
>
) : (
)}
>
)
}
// Error state with progress bar (when showProgress was true during upload)
const TransferErrorWithProgress = ({
transferItem,
onRemove,
}: {
transferItem: TFileAndTransfer
onRemove: () => void
}) => {
// Use lastProgress to show where the upload failed (default to 100% if not available)
const progressValue = (transferItem.lastProgress ?? 1) * 100
return (
<>
{transferItem.attributes['targetFilename']}
{formatFileSize(transferItem.file.size)}
{transferItem.errorMessage && (
{transferItem.errorMessage}
)}
>
)
}
// Error state without progress bar (indeterminate upload)
const TransferErrorIndeterminate = ({
transferItem,
onRemove,
}: {
transferItem: TFileAndTransfer
onRemove: () => void
}) => (
<>
{transferItem.attributes['targetFilename']}
{transferItem.errorMessage && (
{transferItem.errorMessage}
)}
>
)
// Main error component - chooses display based on showProgress
export const TransferError = ({ transferItem, onRemove }: { transferItem: TFileAndTransfer; onRemove: () => void }) => {
// If showProgress was true, show error with progress bar, otherwise show indeterminate error
if (transferItem.showProgress) {
return
}
return
}
export const OperationButton = ({
operation,
onActivate,
transferItem,
}: {
operation: TQueueItemOperation
onActivate: (fileId: string, operation: TQueueItemOperation) => void
transferItem: TFileAndTransfer
}) => {
const title = typeof operation.title === 'function' ? operation.title(transferItem) : operation.title
// Don't render button if title is empty (e.g., comment exists, show icons instead)
if (!title) return null
const onClick = () => {
if (operation.renderInlineUI || operation.renderExtendedUI) {
onActivate(transferItem.fileId, operation)
}
if (operation.onClick) {
operation.onClick(transferItem)
}
}
return (
)
}
export const FilenameRenderer = ({
transferItem,
}: {
transferItem: TFileAndTransfer
queueItemOperations?: Array
onPreviewClick?: () => void
}) => (
<>
{transferItem.attributes['targetFilename']}
{formatFileSize(transferItem.file.size)}
>
)
export const ThumbnailRenderer = ({
transferItem,
onPreviewClick,
}: {
transferItem: TFileAndTransfer
queueItemOperations?: Array
onPreviewClick?: () => void
}) => {
const [thumbnailUrl, setThumbnailUrl] = useState(null)
const [thumbnailLoadFailed, setThumbnailLoadFailed] = useState(false)
const file = transferItem.file
useEffect(() => {
setThumbnailLoadFailed(false)
// Check if the file is an image
if (file.type.startsWith('image/')) {
// Create an object URL for the file
const url = URL.createObjectURL(file)
setThumbnailUrl(url)
// Cleanup: revoke the object URL when component unmounts
return () => {
URL.revokeObjectURL(url)
}
}
setThumbnailUrl(null)
}, [file])
const img = (thumbnailUrl && !thumbnailLoadFailed && (
setThumbnailLoadFailed(true)}
/>
)) ||
const showExpandButton = !!onPreviewClick
const hasThumbnailPreview = !!thumbnailUrl && !thumbnailLoadFailed
return (
{showExpandButton && (
)}
{img}
{transferItem.attributes['targetFilename']}
)
}
// Image Preview Modal Component
// TODO: When we get an actual PktLightBox component, we can remove this component and use that instead.
interface IImagePreviewModal {
isOpen: boolean
images: TFileAndTransfer[]
currentIndex: number
onClose: () => void
onNavigate: (direction: 'prev' | 'next') => void
}
export const ImagePreviewModal = forwardRef(
({ isOpen, images, currentIndex, onClose, onNavigate }: IImagePreviewModal, ref: ForwardedRef) => {
const [imageUrl, setImageUrl] = useState(null)
const [imageLoadFailed, setImageLoadFailed] = useState(false)
const currentImage = images[currentIndex]
const hasMultipleImages = images.length > 1
useEffect(() => {
setImageLoadFailed(false)
if (currentImage && currentImage.file.type.startsWith('image/')) {
const url = URL.createObjectURL(currentImage.file)
setImageUrl(url)
return () => URL.revokeObjectURL(url)
}
setImageUrl(null)
}, [currentImage])
// Keyboard navigation
useEffect(() => {
if (!isOpen) return
const handleKeyDown = (e: KeyboardEvent) => {
switch (e.key) {
case 'ArrowLeft':
e.preventDefault()
onNavigate('prev')
break
case 'ArrowRight':
e.preventDefault()
onNavigate('next')
break
case 'Escape':
e.preventDefault()
onClose()
break
}
}
window.addEventListener('keydown', handleKeyDown)
return () => window.removeEventListener('keydown', handleKeyDown)
}, [isOpen, onNavigate, onClose])
if (!currentImage) return null
return (
{hasMultipleImages && (
onNavigate('prev')}
aria-label={`Forrige bilde ${currentIndex} / ${images.length}`}
/>
)}
{imageUrl && !imageLoadFailed ? (
![{`${currentImage.attributes['targetFilename']}]({imageUrl})
setImageLoadFailed(true)}
/>
) : (
)}
{hasMultipleImages && (
{currentIndex + 1} / {images.length}
)}
{hasMultipleImages && (
onNavigate('next')}
aria-label={`Neste bilde ${currentIndex + 2} / ${images.length}`}
/>
)}
)
},
)
ImagePreviewModal.displayName = 'ImagePreviewModal'