mirror of
https://github.com/Lifeforge-app/lifeforge.git
synced 2026-06-28 06:46:24 +00:00
195 lines
4.6 KiB
TypeScript
195 lines
4.6 KiB
TypeScript
import { useCallback } from 'react'
|
|
import { z } from 'zod'
|
|
|
|
import { Flex, Icon, Text } from '@/components/primitives'
|
|
import { useModalStore } from '@/providers'
|
|
import { colorWithOpacity } from '@/system'
|
|
|
|
import { useInputLabel } from '../shared/hooks/useInputLabel'
|
|
import { FilePickerModal } from './FilePickerModal'
|
|
import { CompactFileDisplay } from './components/CompactFileDisplay'
|
|
import { EmptyFileInput } from './components/EmptyFileInput'
|
|
import { PreviewFileDisplay } from './components/PreviewFileDisplay'
|
|
|
|
export type FileValue =
|
|
| {
|
|
type: 'empty'
|
|
}
|
|
| {
|
|
type: 'existing'
|
|
id: string
|
|
filename: string
|
|
preview?: string
|
|
}
|
|
| {
|
|
type: 'upload'
|
|
file: File
|
|
preview?: string
|
|
}
|
|
| {
|
|
type: 'url'
|
|
url: string
|
|
preview?: string
|
|
}
|
|
|
|
export const fileValueSchema = z.discriminatedUnion('type', [
|
|
z.object({
|
|
type: z.literal('empty')
|
|
}),
|
|
z.object({
|
|
type: z.literal('existing'),
|
|
id: z.string(),
|
|
filename: z.string(),
|
|
preview: z.string().optional()
|
|
}),
|
|
z.object({
|
|
type: z.literal('upload'),
|
|
file: z.instanceof(File),
|
|
preview: z.string().optional()
|
|
}),
|
|
z.object({
|
|
type: z.literal('url'),
|
|
url: z.string(),
|
|
preview: z.string().optional()
|
|
})
|
|
])
|
|
|
|
export interface FilePickerSourceConfig {
|
|
pixabay?: boolean
|
|
url?: boolean
|
|
ai?:
|
|
| {
|
|
defaultPrompt: string
|
|
}
|
|
| false
|
|
}
|
|
|
|
export function FileInput({
|
|
icon,
|
|
label,
|
|
reminderText,
|
|
value,
|
|
onChange,
|
|
onImageRemoved,
|
|
required,
|
|
namespace,
|
|
disabled,
|
|
sources = {
|
|
pixabay: false,
|
|
ai: false,
|
|
url: false
|
|
},
|
|
mimeTypes,
|
|
errorMsg
|
|
}: {
|
|
icon: string
|
|
label: string
|
|
reminderText?: string
|
|
value: FileValue
|
|
onChange: (value: FileValue) => void
|
|
onImageRemoved?: () => void
|
|
required?: boolean
|
|
namespace?: string | false
|
|
disabled?: boolean
|
|
sources?: FilePickerSourceConfig
|
|
mimeTypes?: Record<string, string[]>
|
|
errorMsg?: string
|
|
}) {
|
|
const { open } = useModalStore()
|
|
const inputLabel = useInputLabel({ namespace, label })
|
|
|
|
const handleFilePickerOpen = useCallback(
|
|
function () {
|
|
open(FilePickerModal, {
|
|
sources,
|
|
mimeTypes,
|
|
onSelect: async function (file: string | File, preview: string | null) {
|
|
if (file instanceof File) {
|
|
onChange({
|
|
type: 'upload',
|
|
file,
|
|
preview: preview ?? undefined
|
|
})
|
|
} else {
|
|
onChange({
|
|
type: 'url',
|
|
url: file,
|
|
preview: preview ?? undefined
|
|
})
|
|
}
|
|
}
|
|
})
|
|
},
|
|
[sources, mimeTypes, onChange]
|
|
)
|
|
|
|
const previewUrl = value.type !== 'empty' ? value.preview : undefined
|
|
|
|
return (
|
|
<Flex direction="column" gap="sm" width="100%">
|
|
<Flex
|
|
shadow
|
|
bg={{
|
|
base: colorWithOpacity('bg-200', '50%'),
|
|
dark: colorWithOpacity('bg-800', '70%')
|
|
}}
|
|
className="__file-input"
|
|
direction="column"
|
|
p="lg"
|
|
r="md"
|
|
style={{
|
|
opacity: disabled ? 0.5 : 1,
|
|
pointerEvents: disabled ? 'none' : 'auto',
|
|
outline: errorMsg ? '2px solid var(--color-dangerous)' : undefined
|
|
}}
|
|
width="100%"
|
|
>
|
|
<Text asChild color={errorMsg ? 'dangerous' : 'muted'}>
|
|
<Flex align="center" gap="sm">
|
|
<Icon icon={icon} size="1.5rem" />
|
|
<Text as="span" color="inherit" weight="medium">
|
|
{inputLabel}{' '}
|
|
{required === true && (
|
|
<Text as="span" color="dangerous">
|
|
*
|
|
</Text>
|
|
)}
|
|
</Text>
|
|
</Flex>
|
|
</Text>
|
|
{value.type === 'empty' ? (
|
|
<EmptyFileInput
|
|
acceptedMimeTypes={mimeTypes}
|
|
reminderText={reminderText}
|
|
onSelect={handleFilePickerOpen}
|
|
/>
|
|
) : (
|
|
<>
|
|
{previewUrl &&
|
|
(previewUrl.startsWith('http') ||
|
|
previewUrl.startsWith('blob:') ||
|
|
previewUrl.startsWith('data:')) ? (
|
|
<PreviewFileDisplay
|
|
previewUrl={previewUrl}
|
|
onChange={onChange}
|
|
onImageRemoved={onImageRemoved}
|
|
/>
|
|
) : (
|
|
<CompactFileDisplay
|
|
value={value}
|
|
onChange={onChange}
|
|
onImageRemoved={onImageRemoved}
|
|
/>
|
|
)}
|
|
</>
|
|
)}
|
|
</Flex>
|
|
{errorMsg && (
|
|
<Text color="dangerous" display="block" px="md" size="sm">
|
|
{errorMsg}
|
|
</Text>
|
|
)}
|
|
</Flex>
|
|
)
|
|
}
|