chore: seminarhof customer project
This commit is contained in:
24
packages/components/package.json
Normal file
24
packages/components/package.json
Normal file
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"name": "@project/components",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"exports": {
|
||||
".": "./src/index.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@mantine/hooks": "^9.2.1",
|
||||
"@pikku/mantine": "^0.12.6",
|
||||
"@pikku/react": "^0.12.5",
|
||||
"react": "^19.2.5",
|
||||
"react-dom": "^19.2.5"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^19.2.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^19",
|
||||
"@types/react-dom": "^19",
|
||||
"typescript": "^5.9"
|
||||
}
|
||||
}
|
||||
181
packages/components/src/FileUploader.tsx
Normal file
181
packages/components/src/FileUploader.tsx
Normal file
@@ -0,0 +1,181 @@
|
||||
import { asI18n } from '@pikku/react'
|
||||
import { useRef, useState } from 'react'
|
||||
import { Button, Group, Paper, Stack, Text } from '@pikku/mantine/core'
|
||||
|
||||
export type UploadRequest = {
|
||||
uploadUrl: string
|
||||
contentKey: string
|
||||
uploadMethod?: 'PUT' | 'POST'
|
||||
uploadHeaders?: Record<string, string>
|
||||
}
|
||||
|
||||
export type UploadedAsset = {
|
||||
contentKey: string
|
||||
fileName: string
|
||||
contentType: string
|
||||
size: number
|
||||
}
|
||||
|
||||
type FileUploaderProps = {
|
||||
accept?: string[]
|
||||
compact?: boolean
|
||||
disabled?: boolean
|
||||
inputTestId?: string
|
||||
idleLabel: string
|
||||
activeLabel?: string
|
||||
uploadingLabel?: string
|
||||
uploadedLabel?: string
|
||||
browseLabel?: string
|
||||
hint?: string
|
||||
requestUpload: (file: File) => Promise<UploadRequest>
|
||||
onUploaded: (asset: UploadedAsset) => void
|
||||
onUploadStateChange?: (uploading: boolean) => void
|
||||
onError?: (error: Error) => void
|
||||
}
|
||||
|
||||
const matchesAccept = (file: File, accept: string[]) =>
|
||||
accept.length === 0 ||
|
||||
accept.some((rule) => {
|
||||
if (rule.endsWith('/*')) {
|
||||
return file.type.startsWith(rule.slice(0, -1))
|
||||
}
|
||||
return file.type === rule
|
||||
})
|
||||
|
||||
export function FileUploader({
|
||||
accept = [],
|
||||
compact = false,
|
||||
disabled = false,
|
||||
inputTestId,
|
||||
idleLabel,
|
||||
activeLabel,
|
||||
uploadingLabel = 'Uploading...',
|
||||
uploadedLabel = 'Uploaded',
|
||||
browseLabel = 'Browse',
|
||||
hint,
|
||||
requestUpload,
|
||||
onUploaded,
|
||||
onUploadStateChange,
|
||||
onError,
|
||||
}: FileUploaderProps) {
|
||||
const inputRef = useRef<HTMLInputElement | null>(null)
|
||||
const [isDragging, setIsDragging] = useState(false)
|
||||
const [isUploading, setIsUploading] = useState(false)
|
||||
const [fileName, setFileName] = useState('')
|
||||
|
||||
const setUploading = (value: boolean) => {
|
||||
setIsUploading(value)
|
||||
onUploadStateChange?.(value)
|
||||
}
|
||||
|
||||
const uploadFile = async (file: File) => {
|
||||
if (!matchesAccept(file, accept)) {
|
||||
onError?.(new Error('Unsupported file type'))
|
||||
return
|
||||
}
|
||||
|
||||
setUploading(true)
|
||||
try {
|
||||
const upload = await requestUpload(file)
|
||||
const response = await fetch(upload.uploadUrl, {
|
||||
method: upload.uploadMethod ?? 'PUT',
|
||||
headers: {
|
||||
...(upload.uploadHeaders ?? {}),
|
||||
...(!upload.uploadHeaders?.['Content-Type'] && file.type
|
||||
? { 'Content-Type': file.type }
|
||||
: {}),
|
||||
},
|
||||
body: file,
|
||||
})
|
||||
if (response.status >= 400) {
|
||||
throw new Error('File upload failed')
|
||||
}
|
||||
|
||||
setFileName(file.name)
|
||||
onUploaded({
|
||||
contentKey: upload.contentKey,
|
||||
fileName: file.name,
|
||||
contentType: file.type,
|
||||
size: file.size,
|
||||
})
|
||||
} catch (error) {
|
||||
onError?.(error instanceof Error ? error : new Error('File upload failed'))
|
||||
} finally {
|
||||
setUploading(false)
|
||||
if (inputRef.current) inputRef.current.value = ''
|
||||
}
|
||||
}
|
||||
|
||||
const label = isUploading
|
||||
? uploadingLabel
|
||||
: fileName
|
||||
? `${uploadedLabel}: ${fileName}`
|
||||
: isDragging
|
||||
? activeLabel ?? idleLabel
|
||||
: idleLabel
|
||||
|
||||
return (
|
||||
<Paper
|
||||
withBorder
|
||||
radius="md"
|
||||
p={compact ? 'xs' : 'md'}
|
||||
style={{
|
||||
borderStyle: 'dashed',
|
||||
cursor: disabled || isUploading ? 'not-allowed' : 'pointer',
|
||||
opacity: disabled ? 0.6 : 1,
|
||||
}}
|
||||
onClick={() => {
|
||||
if (!disabled && !isUploading) inputRef.current?.click()
|
||||
}}
|
||||
onDragOver={(e) => {
|
||||
e.preventDefault()
|
||||
if (!disabled && !isUploading) setIsDragging(true)
|
||||
}}
|
||||
onDragLeave={(e) => {
|
||||
e.preventDefault()
|
||||
setIsDragging(false)
|
||||
}}
|
||||
onDrop={(e) => {
|
||||
e.preventDefault()
|
||||
setIsDragging(false)
|
||||
if (disabled || isUploading) return
|
||||
const file = e.dataTransfer.files?.[0]
|
||||
if (file) void uploadFile(file)
|
||||
}}
|
||||
>
|
||||
<Stack gap={compact ? 4 : 'xs'}>
|
||||
<Group justify="space-between" gap="xs" wrap="nowrap">
|
||||
<Text fw={500} size={compact ? 'xs' : 'sm'}>{asI18n(`${label}`)}</Text>
|
||||
<Button
|
||||
size="xs"
|
||||
variant="light"
|
||||
loading={isUploading}
|
||||
disabled={disabled}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
inputRef.current?.click()
|
||||
}}
|
||||
>
|
||||
{asI18n(`${browseLabel}`)}
|
||||
</Button>
|
||||
</Group>
|
||||
{hint && (
|
||||
<Text c="dimmed" size={compact ? 'xs' : 'sm'}>
|
||||
{asI18n(`${hint}`)}
|
||||
</Text>
|
||||
)}
|
||||
</Stack>
|
||||
<input
|
||||
ref={inputRef}
|
||||
hidden
|
||||
type="file"
|
||||
accept={accept.join(',')}
|
||||
data-testid={inputTestId}
|
||||
onChange={(e) => {
|
||||
const file = e.currentTarget.files?.[0]
|
||||
if (file) void uploadFile(file)
|
||||
}}
|
||||
/>
|
||||
</Paper>
|
||||
)
|
||||
}
|
||||
5
packages/components/src/index.ts
Normal file
5
packages/components/src/index.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export {
|
||||
FileUploader,
|
||||
type UploadRequest,
|
||||
type UploadedAsset,
|
||||
} from './FileUploader.js'
|
||||
Reference in New Issue
Block a user