mirror of
https://github.com/ArthurDanjou/ui.git
synced 2026-01-14 12:14:41 +01:00
up
This commit is contained in:
@@ -35,6 +35,7 @@ const components = [
|
|||||||
'command-palette',
|
'command-palette',
|
||||||
'drawer',
|
'drawer',
|
||||||
'dropdown-menu',
|
'dropdown-menu',
|
||||||
|
'file-upload',
|
||||||
'form',
|
'form',
|
||||||
'form-field',
|
'form-field',
|
||||||
'input',
|
'input',
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ const components = [
|
|||||||
'command-palette',
|
'command-palette',
|
||||||
'drawer',
|
'drawer',
|
||||||
'dropdown-menu',
|
'dropdown-menu',
|
||||||
|
'file-upload',
|
||||||
'form',
|
'form',
|
||||||
'form-field',
|
'form-field',
|
||||||
'input',
|
'input',
|
||||||
|
|||||||
@@ -1,5 +1,135 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import * as z from 'zod'
|
||||||
|
import type { FormSubmitEvent } from '@nuxt/ui'
|
||||||
|
|
||||||
|
const MAX_FILE_SIZE = 2 * 1024 * 1024 // 2MB
|
||||||
|
const MIN_DIMENSIONS = { width: 200, height: 200 }
|
||||||
|
const MAX_DIMENSIONS = { width: 4096, height: 4096 }
|
||||||
|
const ACCEPTED_IMAGE_TYPES = ['image/jpeg', 'image/jpg', 'image/png', 'image/webp']
|
||||||
|
|
||||||
|
const formatBytes = (bytes: number, decimals = 2) => {
|
||||||
|
if (bytes === 0) return '0 Bytes'
|
||||||
|
const k = 1024
|
||||||
|
const dm = decimals < 0 ? 0 : decimals
|
||||||
|
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']
|
||||||
|
const i = Math.floor(Math.log(bytes) / Math.log(k))
|
||||||
|
return Number.parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i]
|
||||||
|
}
|
||||||
|
|
||||||
|
const schema = z.object({
|
||||||
|
avatar: z
|
||||||
|
.instanceof(File, {
|
||||||
|
message: 'Please select an image file.'
|
||||||
|
})
|
||||||
|
.refine(file => file.size <= MAX_FILE_SIZE, {
|
||||||
|
message: `The image is too large. Please choose an image smaller than ${formatBytes(MAX_FILE_SIZE)}.`
|
||||||
|
})
|
||||||
|
.refine(file => ACCEPTED_IMAGE_TYPES.includes(file.type), {
|
||||||
|
message: 'Please upload a valid image file (JPEG, PNG, or WebP).'
|
||||||
|
})
|
||||||
|
.refine(
|
||||||
|
file =>
|
||||||
|
new Promise((resolve) => {
|
||||||
|
const reader = new FileReader()
|
||||||
|
reader.onload = (e) => {
|
||||||
|
const img = new Image()
|
||||||
|
img.onload = () => {
|
||||||
|
const meetsDimensions
|
||||||
|
= img.width >= MIN_DIMENSIONS.width
|
||||||
|
&& img.height >= MIN_DIMENSIONS.height
|
||||||
|
&& img.width <= MAX_DIMENSIONS.width
|
||||||
|
&& img.height <= MAX_DIMENSIONS.height
|
||||||
|
resolve(meetsDimensions)
|
||||||
|
}
|
||||||
|
img.src = e.target?.result as string
|
||||||
|
}
|
||||||
|
reader.readAsDataURL(file)
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
message: `The image dimensions are invalid. Please upload an image between ${MIN_DIMENSIONS.width}x${MIN_DIMENSIONS.height} and ${MAX_DIMENSIONS.width}x${MAX_DIMENSIONS.height} pixels.`
|
||||||
|
}
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
type schema = z.output<typeof schema>
|
||||||
|
|
||||||
|
const state = reactive<Partial<schema>>({
|
||||||
|
avatar: undefined
|
||||||
|
})
|
||||||
|
|
||||||
|
const upload = useUpload('/api/blob', { method: 'PUT' })
|
||||||
|
|
||||||
|
async function onSubmit(event: FormSubmitEvent<schema>) {
|
||||||
|
const res = await upload(event.data.avatar)
|
||||||
|
|
||||||
|
console.log(res)
|
||||||
|
}
|
||||||
|
|
||||||
|
const files = ref<File[]>([
|
||||||
|
{
|
||||||
|
name: 'image-01.jpg',
|
||||||
|
size: 1528737,
|
||||||
|
type: 'image/jpeg',
|
||||||
|
url: 'https://picsum.photos/1000/800?grayscale&random=1',
|
||||||
|
id: 'image-01-123456789'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'image-02.jpg',
|
||||||
|
size: 1528737,
|
||||||
|
type: 'image/jpeg',
|
||||||
|
url: 'https://picsum.photos/1000/800?grayscale&random=2',
|
||||||
|
id: 'image-02-123456789'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'image-03.jpg',
|
||||||
|
size: 1528737,
|
||||||
|
type: 'image/jpeg',
|
||||||
|
url: 'https://picsum.photos/1000/800?grayscale&random=3',
|
||||||
|
id: 'image-03-123456789'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'image-04.jpg',
|
||||||
|
size: 1528737,
|
||||||
|
type: 'image/jpeg',
|
||||||
|
url: 'https://picsum.photos/1000/800?grayscale&random=4',
|
||||||
|
id: 'image-04-123456789'
|
||||||
|
}
|
||||||
|
])
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div class="flex flex-col gap-4">
|
||||||
<UFileUpload />
|
<UForm :schema="schema" :state="state" class="space-y-4 w-80" @submit="onSubmit">
|
||||||
|
<UFormField name="avatar" label="Avatar" description="JPG, GIF or PNG. 1MB Max.">
|
||||||
|
<UFileUpload v-slot="{ open, reset, previewUrls }" v-model="state.avatar" accept="image/*">
|
||||||
|
<div class="flex flex-wrap items-center gap-3">
|
||||||
|
<UAvatar size="lg" :src="previewUrls?.[0]" icon="i-lucide-image" />
|
||||||
|
|
||||||
|
<UButton :label="state.avatar ? 'Change image' : 'Upload image'" color="neutral" @click="open()" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p v-if="state.avatar" class="text-xs text-muted mt-1.5">
|
||||||
|
{{ state.avatar.name }}
|
||||||
|
|
||||||
|
<UButton
|
||||||
|
label="Remove"
|
||||||
|
color="error"
|
||||||
|
variant="link"
|
||||||
|
size="xs"
|
||||||
|
class="p-0"
|
||||||
|
@click="reset()"
|
||||||
|
/>
|
||||||
|
</p>
|
||||||
|
</UFileUpload>
|
||||||
|
</UFormField>
|
||||||
|
|
||||||
|
<UButton label="Submit" type="submit" />
|
||||||
|
</UForm>
|
||||||
|
|
||||||
|
<UFileUpload v-slot="{ files, open }" multiple>
|
||||||
|
{{ files?.[0]?.name }}
|
||||||
|
|
||||||
|
<UButton label="Upload" color="neutral" @click="open()" />
|
||||||
|
</UFileUpload>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,81 +1,11 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import * as z from 'zod'
|
|
||||||
import type { FormSubmitEvent } from '@nuxt/ui'
|
|
||||||
|
|
||||||
const MAX_FILE_SIZE = 2 * 1024 * 1024 // 2MB
|
|
||||||
const MIN_DIMENSIONS = { width: 200, height: 200 }
|
|
||||||
const MAX_DIMENSIONS = { width: 4096, height: 4096 }
|
|
||||||
const ACCEPTED_IMAGE_TYPES = ['image/jpeg', 'image/jpg', 'image/png', 'image/webp']
|
|
||||||
|
|
||||||
const formatBytes = (bytes: number, decimals = 2) => {
|
|
||||||
if (bytes === 0) return '0 Bytes'
|
|
||||||
const k = 1024
|
|
||||||
const dm = decimals < 0 ? 0 : decimals
|
|
||||||
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']
|
|
||||||
const i = Math.floor(Math.log(bytes) / Math.log(k))
|
|
||||||
return Number.parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i]
|
|
||||||
}
|
|
||||||
|
|
||||||
const schema = z.object({
|
|
||||||
avatar: z
|
|
||||||
.instanceof(File, {
|
|
||||||
message: 'Please select an image file.'
|
|
||||||
})
|
|
||||||
.refine(file => file.size <= MAX_FILE_SIZE, {
|
|
||||||
message: `The image is too large. Please choose an image smaller than ${formatBytes(MAX_FILE_SIZE)}.`
|
|
||||||
})
|
|
||||||
.refine(file => ACCEPTED_IMAGE_TYPES.includes(file.type), {
|
|
||||||
message: 'Please upload a valid image file (JPEG, PNG, or WebP).'
|
|
||||||
})
|
|
||||||
.refine(
|
|
||||||
file =>
|
|
||||||
new Promise((resolve) => {
|
|
||||||
const reader = new FileReader()
|
|
||||||
reader.onload = (e) => {
|
|
||||||
const img = new Image()
|
|
||||||
img.onload = () => {
|
|
||||||
const meetsDimensions
|
|
||||||
= img.width >= MIN_DIMENSIONS.width
|
|
||||||
&& img.height >= MIN_DIMENSIONS.height
|
|
||||||
&& img.width <= MAX_DIMENSIONS.width
|
|
||||||
&& img.height <= MAX_DIMENSIONS.height
|
|
||||||
resolve(meetsDimensions)
|
|
||||||
}
|
|
||||||
img.src = e.target?.result as string
|
|
||||||
}
|
|
||||||
reader.readAsDataURL(file)
|
|
||||||
}),
|
|
||||||
{
|
|
||||||
message: `The image dimensions are invalid. Please upload an image between ${MIN_DIMENSIONS.width}x${MIN_DIMENSIONS.height} and ${MAX_DIMENSIONS.width}x${MAX_DIMENSIONS.height} pixels.`
|
|
||||||
}
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
type schema = z.output<typeof schema>
|
|
||||||
|
|
||||||
const state = reactive<Partial<schema>>({
|
|
||||||
avatar: undefined
|
|
||||||
})
|
|
||||||
|
|
||||||
const upload = useUpload('/api/blob', { method: 'PUT' })
|
|
||||||
|
|
||||||
async function onSubmit(event: FormSubmitEvent<schema>) {
|
|
||||||
const res = await upload(event.data.avatar)
|
|
||||||
|
|
||||||
console.log(res)
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<UForm :schema="schema" :state="state" class="flex flex-col gap-4" @submit="onSubmit">
|
<div class="text-center">
|
||||||
<UFormField name="avatar" label="Avatar" description="JPG, GIF or PNG. 1MB Max.">
|
<h1 class="font-semibold text-primary mb-1">
|
||||||
<UFileUpload v-slot="{ open, previewUrls }" v-model="state.avatar" class="flex flex-wrap items-center gap-3" accept="image/*">
|
Playground
|
||||||
<UAvatar size="lg" :src="previewUrls?.[0]" icon="i-lucide-image" />
|
</h1>
|
||||||
|
|
||||||
<UButton label="Choose" color="neutral" @click="open()" />
|
<div class="flex items-center justify-center gap-1">
|
||||||
</UFileUpload>
|
<UKbd value="meta" /> <UKbd value="K" />
|
||||||
</UFormField>
|
</div>
|
||||||
|
</div>
|
||||||
<UButton label="Submit" type="submit" block />
|
|
||||||
</UForm>
|
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ export interface FileUploadProps<M extends boolean = false> {
|
|||||||
* @defaultValue false
|
* @defaultValue false
|
||||||
*/
|
*/
|
||||||
dropzone?: boolean
|
dropzone?: boolean
|
||||||
|
defaultValue?: File[]
|
||||||
class?: any
|
class?: any
|
||||||
ui?: FileUpload['slots']
|
ui?: FileUpload['slots']
|
||||||
}
|
}
|
||||||
@@ -46,6 +47,7 @@ export interface FileUploadSlots {
|
|||||||
open: UseFileDialogReturn['open']
|
open: UseFileDialogReturn['open']
|
||||||
reset: UseFileDialogReturn['reset']
|
reset: UseFileDialogReturn['reset']
|
||||||
previewUrls: string[]
|
previewUrls: string[]
|
||||||
|
files: FileList[]
|
||||||
}): any
|
}): any
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
@@ -73,9 +75,8 @@ const appConfig = useAppConfig() as FileUpload['AppConfig']
|
|||||||
|
|
||||||
const { emitFormInput, emitFormChange, id, name, disabled, ariaAttrs } = useFormField<FileUploadProps>(props, { deferInputValidation: true })
|
const { emitFormInput, emitFormChange, id, name, disabled, ariaAttrs } = useFormField<FileUploadProps>(props, { deferInputValidation: true })
|
||||||
|
|
||||||
// eslint-disable-next-line vue/no-dupe-keys
|
|
||||||
const ui = computed(() => tv({ extend: tv(theme), ...(appConfig.ui?.fileUpload || {}) })({
|
const ui = computed(() => tv({ extend: tv(theme), ...(appConfig.ui?.fileUpload || {}) })({
|
||||||
|
dropzone: props.dropzone
|
||||||
}))
|
}))
|
||||||
|
|
||||||
const inputRef = ref<HTMLInputElement>()
|
const inputRef = ref<HTMLInputElement>()
|
||||||
@@ -84,14 +85,14 @@ const { files, open, reset, onCancel, onChange } = useFileDialog({
|
|||||||
multiple: props.multiple,
|
multiple: props.multiple,
|
||||||
accept: props.accept,
|
accept: props.accept,
|
||||||
reset: props.reset,
|
reset: props.reset,
|
||||||
input: inputRef.value
|
input: inputRef.value,
|
||||||
|
initialFiles: props.defaultValue
|
||||||
})
|
})
|
||||||
|
|
||||||
const previewUrls = computed(() => Array.from(files.value || []).map(file => URL.createObjectURL(file)))
|
const previewUrls = computed(() => Array.from(files.value || []).map(file => URL.createObjectURL(file)))
|
||||||
|
|
||||||
onChange((files) => {
|
onChange((files) => {
|
||||||
console.log('files:', typeof files?.[0])
|
modelValue.value = (props.multiple ? files : files?.[0]) as (M extends true ? File[] : File) | null
|
||||||
modelValue.value = props.multiple ? files : files?.[0]
|
|
||||||
})
|
})
|
||||||
|
|
||||||
onCancel(() => {
|
onCancel(() => {
|
||||||
@@ -101,7 +102,7 @@ onCancel(() => {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Primitive :as="as" :class="ui.root({ class: [props.ui?.root, props.class] })">
|
<Primitive :as="as" :class="ui.root({ class: [props.ui?.root, props.class] })">
|
||||||
<slot :open="open" :reset="reset" :preview-urls="previewUrls" />
|
<slot :open="open" :files="files" :reset="reset" :preview-urls="previewUrls" />
|
||||||
|
|
||||||
<input
|
<input
|
||||||
:id="id"
|
:id="id"
|
||||||
@@ -113,6 +114,7 @@ onCancel(() => {
|
|||||||
:required="required"
|
:required="required"
|
||||||
:disabled="disabled"
|
:disabled="disabled"
|
||||||
hidden
|
hidden
|
||||||
|
tabindex="-1"
|
||||||
v-bind="{ ...$attrs, ...ariaAttrs }"
|
v-bind="{ ...$attrs, ...ariaAttrs }"
|
||||||
>
|
>
|
||||||
</Primitive>
|
</Primitive>
|
||||||
|
|||||||
@@ -1,5 +1,11 @@
|
|||||||
export default {
|
export default {
|
||||||
slots: {
|
slots: {
|
||||||
root: ''
|
root: '',
|
||||||
|
base: ''
|
||||||
|
},
|
||||||
|
variants: {
|
||||||
|
dropzone: {
|
||||||
|
base: 'border-dashed'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user