chore: seminarhof customer project

This commit is contained in:
e2e
2026-07-10 23:45:12 +02:00
commit 5a060fd8f2
188 changed files with 20779 additions and 0 deletions

View 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"
}
}

View 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>
)
}

View File

@@ -0,0 +1,5 @@
export {
FileUploader,
type UploadRequest,
type UploadedAsset,
} from './FileUploader.js'