Skip to content

Instantly share code, notes, and snippets.

@bokarios
Last active September 29, 2022 09:54
Show Gist options
  • Save bokarios/e3f23835aa7ec7bedac4814b43303ed7 to your computer and use it in GitHub Desktop.
Save bokarios/e3f23835aa7ec7bedac4814b43303ed7 to your computer and use it in GitHub Desktop.
Drag and Drop Uploader
<template>
<div
:data-active="active"
@dragenter.prevent="setActive"
@dragover.prevent="setActive"
@dragleave.prevent="setInactive"
@drop.prevent="onDrop"
>
<slot :dropZoneActive="active"></slot>
</div>
</template>
<script setup>
import { ref, onMounted, onUnmounted } from 'vue'
const emit = defineEmits(['files-dropped'])
let active = ref(false)
let inActiveTimeout = null
function setActive() {
active.value = true
clearTimeout(inActiveTimeout)
}
function setInactive() {
inActiveTimeout = setTimeout(() => {
active.value = false
}, 50)
}
function onDrop(e) {
setInactive()
emit('files-dropped', [...e.dataTransfer.files])
}
function preventDefaults(e) {
e.preventDefault()
}
const events = ['dragenter', 'dragover', 'dragleave', 'drop']
onMounted(() => {
events.forEach((eventName) => {
document.body.addEventListener(eventName, preventDefaults)
})
})
onUnmounted(() => {
events.forEach((eventName) => {
document.body.removeEventListener(eventName, preventDefaults)
})
})
</script>
<template>
<div
class="fixed w-screen h-screen overflow-hidden flex justify-center items-center z-50 backdrop-blur-sm backdrop-brightness-90"
@click.self="$emit('close-modal')"
>
<div class="card w-[420px]">
<div class="flex justify-center mb-5">
<h4
class="capitalize text-brand-blue-600 font-semibold dark:text-brand-scooter-500"
>
{{ props.type.title }}
</h4>
</div>
<drop-zone
class="px-5"
@files-dropped="addFiles"
#default="{ dropZoneActive }"
>
<div
class="mt-1 flex justify-center px-6 pt-5 pb-6 border-2 border-gray-300 border-dashed dark:border-brand-scooter-300 rounded-md"
>
<div class="space-y-1 text-center">
<image-upload-icon class="mx-auto h-12 w-12 text-gray-400" />
<div class="flex text-sm text-gray-600 dark:text-brand-scooter-200">
<div
v-if="dropZoneActive"
for="file-upload"
class="relative flex flex-col items-center w-full cursor-pointer bg-white rounded-md font-medium text-brand-teal-600 hover:text-indigo-500 focus-within:outline-none focus-within:ring-2 focus-within:ring-offset-2 focus-within:ring-indigo-500"
>
<span>Drop a file</span>
<input
id="file-upload"
name="file-upload"
type="file"
class="sr-only"
@change="onInputChange"
/>
</div>
<p v-else class="pl-1">Drag files here to upload</p>
</div>
<p class="text-xs text-gray-500 dark:text-brand-scooter-400">
PNG, JPG, JPEG up to 5MB
</p>
</div>
</div>
<div v-if="files.length" class="row px-4">
<div
v-for="file in files"
:key="file.id"
class="w-full sm:w-1/2 p-1 flex-1"
>
<file-preview :file="file" tag="div" />
</div>
</div>
</drop-zone>
<div class="flex justify-end pr-5 mt-8">
<button
class="uppercase text-sm flex justify-center items-center px-8 h-10 bg-brand-blue-500 dark:bg-gray-600 hover:opacity-80 text-gray-50 rounded"
@click="uploadCover"
>
upload
</button>
</div>
</div>
</div>
</template>
<script setup>
import DropZone from '../DropZone.vue'
import FilePreview from '../FilePreview.vue'
import ImageUploadIcon from '../icons/ImageUploadIcon.vue'
import useFileList from '../compositions/file-list'
import createUploader from '../compositions/file-uploader'
import { useToast } from 'vue-toastification'
const { files, addFiles, onInputChange } = useFileList()
const props = defineProps({
type: { required: true },
})
const emit = defineEmits(['close-modal'])
const toast = useToast()
async function uploadCover() {
if (files.value.length > 0) {
try {
const { uploadFile } = createUploader(props.type.url)
const res = await uploadFile(files.value[0])
if (res.status === 200) {
toast.success('cover upload successfully')
emit('close-modal')
}
} catch (error) {
toast.error('server is down, try again later')
throw error
}
}
}
</script>
import { ref } from 'vue'
export default function () {
const files = ref([])
function addFiles(newFiles) {
let newUploadableFiles = [...newFiles]
.map((file) => new UploadableFile(file))
.filter((file) => !fileExists(file.id))
files.value = files.value.concat(newUploadableFiles)
}
function fileExists(otherId) {
return files.value.some(({ id }) => id === otherId)
}
function removeFile(file) {
const index = files.value.indexOf(file)
if (index > -1) files.value.splice(index, 1)
}
function onInputChange(e) {
addFiles(e.target.files)
e.target.value = null
}
return { files, addFiles, removeFile, onInputChange }
}
class UploadableFile {
constructor(file) {
this.file = file
this.name = file.name.split('.')[0]
this.id = `${file.name}-${file.size}-${file.lastModified}-${file.type}`
this.url = URL.createObjectURL(file)
this.status = null
this.ext = file.name.split('.').pop()
}
}
import axios from '../../plugins/axios'
export async function uploadFile(file, url) {
let formData = new FormData()
formData.append('file', file.file)
file.status = 'loading'
let response = await axios.post(url, formData)
file.status = response.ok
return response
}
export function uploadFiles(files, url) {
return Promise.all(files.map((file) => uploadFile(file, url)))
}
export default function createUploader(url) {
return {
uploadFile: function (file) {
return uploadFile(file, url)
},
uploadFiles: function (files) {
return uploadFiles(files, url)
},
}
}
<template>
<component :is="tag" class="aspect-auto">
<div v-if="doc" class="flex items-center gap-2 mt-1">
<div
class="flex flex-col items-center justify-center bg-gray-50 dark:bg-gray-700 p-2 w-[75px] h-[90px] shadow-md drop-shadow-sm rounded-lg"
>
<document-icon
class="w-20 h-20 text-brand-blue-500 dark:text-brand-scooter-300"
/>
<span class="text-brand-blue-600 dark:text-brand-scooter-400">{{
file.ext
}}</span>
</div>
<span
class="text-xs text-brand-blue-800 font-semibold dark:text-brand-scooter-500"
>{{ file.name }}</span
>
</div>
<div v-if="video" class="flex items-center gap-2 mt-1">
<video autoplay="false" preload="metadata" class="w-[100px] h-[90px]">
<source :src="file.url" />
</video>
<span
class="text-xs text-brand-blue-800 font-semibold dark:text-brand-scooter-500"
>{{ file.name + '.' + file.ext }}</span
>
</div>
<img
v-if="!video && !doc"
:src="file.url"
:alt="file.file.name"
:title="file.file.name"
/>
</component>
</template>
<script setup>
import DocumentIcon from '../components/icons/DocumentIcon.vue'
defineProps({
file: { type: Object, required: true },
tag: { type: String, default: 'li' },
video: { type: Boolean, default: false },
doc: { type: Boolean, default: false },
})
</script>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment