{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "name": "dropzone-nuxtjs", "type": "registry:block", "title": "Dropzone (File Upload) for Nuxt and Supabase", "description": "Displays a control for easier uploading of files directly to Supabase Storage.", "dependencies": [ "@supabase/supabase-js@latest", "@vueuse/core", "lucide-vue-next" ], "registryDependencies": [ "button" ], "files": [ { "path": "registry/default/dropzone/nuxtjs/app/components/dropzone.vue", "content": "\n\n\n", "type": "registry:component", "target": "app/components/dropzone.vue" }, { "path": "registry/default/dropzone/nuxtjs/app/components/dropzone-empty-state.vue", "content": "\n\n\n", "type": "registry:component", "target": "app/components/dropzone-empty-state.vue" }, { "path": "registry/default/dropzone/nuxtjs/app/components/dropzone-content.vue", "content": "\n\n\n", "type": "registry:component", "target": "app/components/dropzone-content.vue" }, { "path": "registry/default/dropzone/nuxtjs/app/composables/useSupabaseUpload.ts", "content": "import { useDropZone } from '@vueuse/core'\nimport { computed, onUnmounted, ref, watch } from 'vue'\n\n// @ts-ignore\nimport { createClient } from '@/lib/supabase/client'\n\nconst supabase = createClient()\n\nexport interface FileWithPreview extends File {\n preview?: string\n errors: { code: string; message: string }[]\n}\n\nexport type UseSupabaseUploadOptions = {\n bucketName: string\n path?: string\n allowedMimeTypes?: string[]\n maxFileSize?: number\n maxFiles?: number\n cacheControl?: number\n upsert?: boolean\n}\n\nfunction validateFileType(file: File, allowedTypes: string[]) {\n if (!allowedTypes.length) return []\n const isValid = allowedTypes.some((t) =>\n t.endsWith('/*') ? file.type.startsWith(t.replace('/*', '')) : file.type === t\n )\n return isValid ? [] : [{ code: 'invalid-type', message: 'Invalid file type' }]\n}\n\nfunction validateFileSize(file: File, maxSize: number) {\n return file.size > maxSize\n ? [{ code: 'file-too-large', message: `File is larger than allowed size` }]\n : []\n}\n\nexport function useSupabaseUpload(options: UseSupabaseUploadOptions) {\n const {\n bucketName,\n path,\n allowedMimeTypes = [],\n maxFileSize = Number.POSITIVE_INFINITY,\n maxFiles = 1,\n cacheControl = 3600,\n upsert = false,\n } = options\n\n const files = ref([])\n const loading = ref(false)\n const errors = ref<{ name: string; message: string }[]>([])\n const successes = ref([])\n\n const isSuccess = computed(() => {\n if (!errors.value.length && !successes.value.length) return false\n return !errors.value.length && successes.value.length === files.value.length\n })\n\n const dropZoneRef = ref(null)\n\n const { isOverDropZone } = useDropZone(dropZoneRef, {\n onDrop(droppedFiles: File[] | null) {\n if (!droppedFiles) return\n\n const newFiles: FileWithPreview[] = droppedFiles.map((file) => ({\n ...(file as FileWithPreview),\n preview: URL.createObjectURL(file),\n errors: [\n ...validateFileType(file, allowedMimeTypes),\n ...validateFileSize(file, maxFileSize),\n ],\n }))\n\n files.value = [...files.value, ...newFiles]\n },\n })\n\n const onUpload = async () => {\n loading.value = true\n\n try {\n const filesWithErrors = errors.value.map((e) => e.name)\n\n const filesToUpload =\n filesWithErrors.length > 0\n ? files.value.filter(\n (f) => filesWithErrors.includes(f.name) || !successes.value.includes(f.name)\n )\n : files.value\n\n const responses = await Promise.all(\n filesToUpload.map(async (file) => {\n const { error } = await supabase.storage\n .from(bucketName)\n .upload(path ? `${path}/${file.name}` : file.name, file, {\n cacheControl: cacheControl.toString(),\n upsert,\n })\n\n return error\n ? { name: file.name, message: error.message }\n : { name: file.name, message: undefined }\n })\n )\n\n errors.value = responses.filter(\n (r): r is { name: string; message: string } => r.message !== undefined\n )\n\n const successful = responses.filter((r) => !r.message).map((r) => r.name)\n\n successes.value = Array.from(new Set([...successes.value, ...successful]))\n } catch (err) {\n console.error('Upload failed unexpectedly:', err)\n\n errors.value.push({\n name: 'upload',\n message: 'An unexpected error occurred during upload.',\n })\n } finally {\n loading.value = false\n }\n }\n\n watch(\n () => files.value.length,\n () => {\n if (!files.value.length) {\n errors.value = []\n successes.value = []\n }\n\n if (files.value.length > maxFiles) {\n errors.value.push({\n name: 'files',\n message: `You may upload up to ${maxFiles} files`,\n })\n }\n }\n )\n\n watch(\n files,\n (newFiles, oldFiles) => {\n const newPreviews = new Set(newFiles.map((f) => f.preview))\n oldFiles.forEach((file) => {\n if (file.preview && !newPreviews.has(file.preview)) {\n URL.revokeObjectURL(file.preview)\n }\n })\n },\n { deep: true }\n )\n\n onUnmounted(() => {\n files.value.forEach((file) => {\n if (file.preview) {\n URL.revokeObjectURL(file.preview)\n }\n })\n })\n\n return {\n dropZoneRef,\n isOverDropZone,\n\n files,\n setFiles: (v: FileWithPreview[]) => (files.value = v),\n\n errors,\n setErrors: (v: { name: string; message: string }[]) => (errors.value = v),\n\n successes,\n isSuccess,\n loading,\n onUpload,\n\n maxFileSize,\n maxFiles,\n allowedMimeTypes,\n }\n}\n", "type": "registry:component", "target": "app/composables/useSupabaseUpload.ts" } ] }