Skip to content

Commit 35dbe6c

Browse files
feat(FileUpload): new component (#4564)
Co-authored-by: Vachmara <55046446+vachmara@users.noreply.github.com>
1 parent 63476e5 commit 35dbe6c

33 files changed

+3557
-13
lines changed
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
<script setup lang="ts">
2+
import * as z from 'zod'
3+
import type { FormSubmitEvent } from '@nuxt/ui'
4+
5+
const MAX_FILE_SIZE = 2 * 1024 * 1024 // 2MB
6+
const MIN_DIMENSIONS = { width: 200, height: 200 }
7+
const MAX_DIMENSIONS = { width: 4096, height: 4096 }
8+
const ACCEPTED_IMAGE_TYPES = ['image/jpeg', 'image/jpg', 'image/png', 'image/webp']
9+
10+
const formatBytes = (bytes: number, decimals = 2) => {
11+
if (bytes === 0) return '0 Bytes'
12+
const k = 1024
13+
const dm = decimals < 0 ? 0 : decimals
14+
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']
15+
const i = Math.floor(Math.log(bytes) / Math.log(k))
16+
return Number.parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i]
17+
}
18+
19+
const schema = z.object({
20+
avatar: z
21+
.instanceof(File, {
22+
message: 'Please select an image file.'
23+
})
24+
.refine(file => file.size <= MAX_FILE_SIZE, {
25+
message: `The image is too large. Please choose an image smaller than ${formatBytes(MAX_FILE_SIZE)}.`
26+
})
27+
.refine(file => ACCEPTED_IMAGE_TYPES.includes(file.type), {
28+
message: 'Please upload a valid image file (JPEG, PNG, or WebP).'
29+
})
30+
.refine(
31+
file =>
32+
new Promise((resolve) => {
33+
const reader = new FileReader()
34+
reader.onload = (e) => {
35+
const img = new Image()
36+
img.onload = () => {
37+
const meetsDimensions
38+
= img.width >= MIN_DIMENSIONS.width
39+
&& img.height >= MIN_DIMENSIONS.height
40+
&& img.width <= MAX_DIMENSIONS.width
41+
&& img.height <= MAX_DIMENSIONS.height
42+
resolve(meetsDimensions)
43+
}
44+
img.src = e.target?.result as string
45+
}
46+
reader.readAsDataURL(file)
47+
}),
48+
{
49+
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.`
50+
}
51+
)
52+
})
53+
54+
type schema = z.output<typeof schema>
55+
56+
const state = reactive<Partial<schema>>({
57+
avatar: undefined
58+
})
59+
60+
function createObjectUrl(file: File): string {
61+
return URL.createObjectURL(file)
62+
}
63+
64+
async function onSubmit(event: FormSubmitEvent<schema>) {
65+
console.log(event.data)
66+
}
67+
</script>
68+
69+
<template>
70+
<UForm :schema="schema" :state="state" class="space-y-4 w-64" @submit="onSubmit">
71+
<UFormField name="avatar" label="Avatar" description="JPG, GIF or PNG. 1MB Max.">
72+
<UFileUpload v-slot="{ open, removeFile }" v-model="state.avatar" accept="image/*">
73+
<div class="flex flex-wrap items-center gap-3">
74+
<UAvatar size="lg" :src="state.avatar ? createObjectUrl(state.avatar) : undefined" icon="i-lucide-image" />
75+
76+
<UButton :label="state.avatar ? 'Change image' : 'Upload image'" color="neutral" variant="outline" @click="open()" />
77+
</div>
78+
79+
<p v-if="state.avatar" class="text-xs text-muted mt-1.5">
80+
{{ state.avatar.name }}
81+
82+
<UButton
83+
label="Remove"
84+
color="error"
85+
variant="link"
86+
size="xs"
87+
class="p-0"
88+
@click="removeFile()"
89+
/>
90+
</p>
91+
</UFileUpload>
92+
</UFormField>
93+
94+
<UButton type="submit" label="Submit" color="neutral" />
95+
</UForm>
96+
</template>
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
<script setup lang="ts">
2+
const value = ref<File[]>([])
3+
</script>
4+
5+
<template>
6+
<UFileUpload
7+
v-model="value"
8+
icon="i-lucide-image"
9+
label="Drop your images here"
10+
description="SVG, PNG, JPG or GIF (max. 2MB)"
11+
layout="list"
12+
multiple
13+
:interactive="false"
14+
class="w-96 min-h-48"
15+
>
16+
<template #actions="{ open }">
17+
<UButton
18+
label="Select images"
19+
icon="i-lucide-upload"
20+
color="neutral"
21+
variant="outline"
22+
@click="open()"
23+
/>
24+
</template>
25+
26+
<template #files-bottom="{ removeFile, files }">
27+
<UButton
28+
v-if="files?.length"
29+
label="Remove all files"
30+
color="neutral"
31+
@click="removeFile()"
32+
/>
33+
</template>
34+
</UFileUpload>
35+
</template>
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
<script setup lang="ts">
2+
const value = ref<File[]>([])
3+
</script>
4+
5+
<template>
6+
<UFileUpload
7+
v-model="value"
8+
icon="i-lucide-image"
9+
label="Drop your images here"
10+
description="SVG, PNG, JPG or GIF (max. 2MB)"
11+
layout="grid"
12+
multiple
13+
:interactive="false"
14+
class="w-96 min-h-48"
15+
>
16+
<template #actions="{ open }">
17+
<UButton
18+
label="Select images"
19+
icon="i-lucide-upload"
20+
color="neutral"
21+
variant="outline"
22+
@click="open()"
23+
/>
24+
</template>
25+
26+
<template #files-top="{ open, files }">
27+
<div v-if="files?.length" class="mb-2 flex items-center justify-between">
28+
<p class="font-bold">
29+
Files ({{ files?.length }})
30+
</p>
31+
32+
<UButton
33+
icon="i-lucide-plus"
34+
label="Add more"
35+
color="neutral"
36+
variant="outline"
37+
class="-my-2"
38+
@click="open()"
39+
/>
40+
</div>
41+
</template>
42+
</UFileUpload>
43+
</template>
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
<script setup lang="ts">
2+
import * as z from 'zod'
3+
import type { FormSubmitEvent } from '@nuxt/ui'
4+
5+
const MAX_FILE_SIZE = 2 * 1024 * 1024 // 2MB
6+
const MIN_DIMENSIONS = { width: 200, height: 200 }
7+
const MAX_DIMENSIONS = { width: 4096, height: 4096 }
8+
const ACCEPTED_IMAGE_TYPES = ['image/jpeg', 'image/jpg', 'image/png', 'image/webp']
9+
10+
const formatBytes = (bytes: number, decimals = 2) => {
11+
if (bytes === 0) return '0 Bytes'
12+
const k = 1024
13+
const dm = decimals < 0 ? 0 : decimals
14+
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']
15+
const i = Math.floor(Math.log(bytes) / Math.log(k))
16+
return Number.parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i]
17+
}
18+
19+
const schema = z.object({
20+
image: z
21+
.instanceof(File, {
22+
message: 'Please select an image file.'
23+
})
24+
.refine(file => file.size <= MAX_FILE_SIZE, {
25+
message: `The image is too large. Please choose an image smaller than ${formatBytes(MAX_FILE_SIZE)}.`
26+
})
27+
.refine(file => ACCEPTED_IMAGE_TYPES.includes(file.type), {
28+
message: 'Please upload a valid image file (JPEG, PNG, or WebP).'
29+
})
30+
.refine(
31+
file =>
32+
new Promise((resolve) => {
33+
const reader = new FileReader()
34+
reader.onload = (e) => {
35+
const img = new Image()
36+
img.onload = () => {
37+
const meetsDimensions
38+
= img.width >= MIN_DIMENSIONS.width
39+
&& img.height >= MIN_DIMENSIONS.height
40+
&& img.width <= MAX_DIMENSIONS.width
41+
&& img.height <= MAX_DIMENSIONS.height
42+
resolve(meetsDimensions)
43+
}
44+
img.src = e.target?.result as string
45+
}
46+
reader.readAsDataURL(file)
47+
}),
48+
{
49+
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.`
50+
}
51+
)
52+
})
53+
54+
type schema = z.output<typeof schema>
55+
56+
const state = reactive<Partial<schema>>({
57+
image: undefined
58+
})
59+
60+
async function onSubmit(event: FormSubmitEvent<schema>) {
61+
console.log(event.data)
62+
}
63+
</script>
64+
65+
<template>
66+
<UForm :schema="schema" :state="state" class="space-y-4 w-96" @submit="onSubmit">
67+
<UFormField name="image" label="Image" description="JPG, GIF or PNG. 2MB Max.">
68+
<UFileUpload v-model="state.image" accept="image/*" class="min-h-48" />
69+
</UFormField>
70+
71+
<UButton type="submit" label="Submit" color="neutral" />
72+
</UForm>
73+
</template>

0 commit comments

Comments
 (0)