Skip to content
Merged
120 changes: 120 additions & 0 deletions app/components/DragAndDrop.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
<template>
<v-hover v-slot="{ isHovering, props: hoverProps }">
<v-card
v-bind="hoverProps"
class="text-center cursor-pointer"
:class="{
'elevation-4': isHovering || isDragging,
'elevation-0': !(isHovering || isDragging),
}"
:style="{
position: 'relative',
overflow: 'hidden',
transition: 'all 0.3s ease',
background:
isHovering || isDragging
? 'rgba(var(--v-theme-primary), 0.05)'
: 'rgba(0, 0, 0, 0.02)',
border: `2px dashed ${
isHovering || isDragging ? 'rgb(var(--v-theme-primary))' : '#e0e0e0'
}`,
transform: isHovering || isDragging ? 'translateY(-2px)' : 'none',
pointerEvents: loading ? 'none' : 'auto',
opacity: loading ? 0.6 : 1,
}"
rounded="xl"
@click="triggerFileDialog"
@dragover.prevent="isDragging = true"
@dragleave.prevent="isDragging = false"
@drop.prevent="handleDrop"
>
<v-card-text class="pa-8">
<v-sheet
class="mx-auto mb-6 d-flex align-center justify-center"
:color="isHovering || isDragging ? 'primary' : 'grey-lighten-2'"
:elevation="isHovering || isDragging ? 4 : 0"
rounded="circle"
width="80"
height="80"
style="transition: all 0.3s ease"
>
<v-icon
:icon="loading ? 'mdi-loading' : 'mdi-cloud-upload'"
size="40"
:color="isHovering || isDragging ? 'white' : 'grey-darken-1'"
:class="{ rotating: loading }"
/>
</v-sheet>

<v-card-title
class="text-h6 font-weight-bold justify-center pa-0 mb-1"
:class="
isHovering || isDragging ? 'text-primary' : 'text-grey-darken-2'
"
style="transition: color 0.3s ease"
>
{{ loading ? loadingText : isDragging ? dropText : idleText }}
</v-card-title>

<v-card-subtitle v-if="showExtensions" class="text-body-2 pa-0">
{{ accept ? `(${accept} files)` : "All files allowed" }}
</v-card-subtitle>
</v-card-text>

<input
ref="fileInput"
type="file"
class="d-none"
:multiple="multiple"
:accept="accept"
@change="handleFileSelect"
/>
</v-card>
</v-hover>
</template>

<script setup>
const props = defineProps({
multiple: { type: Boolean, default: false },
accept: { type: String, default: "" },
loading: { type: Boolean, default: false },
showExtensions: { type: Boolean, default: true },
idleText: { type: String, default: "Click or Drag & Drop files" },
dropText: { type: String, default: "Drop to upload" },
loadingText: { type: String, default: "Uploading..." },
})

const emit = defineEmits(["files-selected"])

const isDragging = ref(false)
const fileInput = ref(null)

const triggerFileDialog = () => fileInput.value?.click()

function handleDrop(e) {
isDragging.value = false
const files = Array.from(e.dataTransfer.files)
emit("files-selected", files)
}

function handleFileSelect(e) {
const files = Array.from(e.target.files)
emit("files-selected", files)
e.target.value = ""
}
</script>

<style scoped>
.rotating {
animation: rotate 1s linear infinite;
}

@keyframes rotate {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
</style>
25 changes: 11 additions & 14 deletions app/components/FileSelector.vue
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import { useGeodeStore } from "@ogw_front/stores/geode"

const schema = schemas.opengeodeweb_back.allowed_files

const emit = defineEmits([
"update_values",
"increment_step",
Expand All @@ -23,27 +24,24 @@

const props = defineProps({
multiple: { type: Boolean, required: true },
files: { type: Array, required: false, default: [] },
auto_upload: { type: Boolean, required: false, default: true },
files: { type: Array, default: () => [] },
auto_upload: { type: Boolean, default: true },
})

const { multiple } = props

const internal_files = ref(props.files)
const auto_upload = ref(props.auto_upload)
const accept = ref("")
const loading = ref(false)

watch(
() => props.files,
(newVal) => {
internal_files.value = newVal
},
{ deep: true },
(val) => (internal_files.value = val),
)

watch(
() => props.auto_upload,
(val) => (auto_upload.value = val),
)
watch(props.auto_upload, (newVal) => {
auto_upload.value = newVal
})

const toggle_loading = useToggle(loading)

Expand All @@ -58,10 +56,9 @@
toggle_loading()
const geodeStore = useGeodeStore()
const response = await geodeStore.request(schema, {})
accept.value = response.extensions
.map((extension) => "." + extension)
.join(",")
accept.value = response.extensions.map((e) => `.${e}`).join(",")
toggle_loading()
}

await get_allowed_files()
</script>
117 changes: 69 additions & 48 deletions app/components/FileUploader.vue
Original file line number Diff line number Diff line change
@@ -1,43 +1,65 @@
<template>
<v-row>
<v-col class="pa-0">
<v-file-input
v-model="internal_files"
:multiple="props.multiple"
:label="label"
:accept="props.accept"
:rules="[(value) => !!value || 'The file is mandatory']"
color="primary"
:hide-input="props.mini"
:hide-details="props.mini"
chips
counter
show-size
@click:clear="clear()"
/>
</v-col>
</v-row>
<v-row v-if="!props.auto_upload">
<v-col cols="auto">
<v-btn
<DragAndDrop
:multiple="props.multiple"
:accept="props.accept"
:loading="loading"
:show-extensions="false"
@files-selected="processSelectedFiles"
/>

<v-card-text v-if="internal_files.length" class="mt-4">
<v-sheet class="d-flex align-center mb-3" color="transparent">
<v-icon icon="mdi-file-check" class="mr-2" color="primary" />
<span class="text-subtitle-2 font-weight-bold"> Selected Files </span>
<v-chip size="x-small" class="ml-2" color="primary" variant="flat">
{{ internal_files.length }}
</v-chip>
</v-sheet>

<v-sheet class="d-flex flex-wrap gap-2" color="transparent">
<v-chip
v-for="(file, index) in internal_files"
:key="index"
closable
size="small"
color="primary"
:disabled="!internal_files.length && !files_uploaded"
:loading="loading"
class="pa-2"
@click="upload_files"
variant="tonal"
class="font-weight-medium"
@click:close="removeFile(index)"
>
<v-icon start size="16">mdi-file-outline</v-icon>
{{ file.name }}
</v-chip>
</v-sheet>
</v-card-text>

<v-card-actions v-if="!props.auto_upload && internal_files.length">
<v-btn
color="primary"
variant="elevated"
size="large"
rounded="lg"
:loading="loading"
class="text-none px-3 font-weight-bold"
@click="upload_files"
>
<v-icon start size="20">mdi-cloud-upload-outline</v-icon>
Upload {{ internal_files.length }} file<span
v-if="internal_files.length > 1"
>s</span
>
Upload file(s)
</v-btn>
</v-col>
</v-row>
</v-btn>
</v-card-actions>
</template>

<script setup>
import schemas from "@geode/opengeodeweb-back/opengeodeweb_back_schemas.json"
import { upload_file } from "@ogw_front/utils/upload_file"
import DragAndDrop from "@ogw_front/components/DragAndDrop"

const schema = schemas.opengeodeweb_back.upload_file

const emit = defineEmits(["files_uploaded", "decrement_step"])
const emit = defineEmits(["files_uploaded", "decrement_step", "reset_values"])

const props = defineProps({
multiple: { type: Boolean, required: true },
Expand All @@ -47,15 +69,28 @@
mini: { type: Boolean, required: false, default: false },
})

const label = props.multiple
? "Please select file(s) to import"
: "Please select a file to import"
const internal_files = ref(props.files)
const loading = ref(false)
const files_uploaded = ref(false)

const toggle_loading = useToggle(loading)

function processSelectedFiles(files) {
if (props.multiple) {
internal_files.value = [...internal_files.value, ...files]
} else {
internal_files.value = [files[0]]
}
}

function removeFile(index) {
internal_files.value.splice(index, 1)
if (internal_files.value.length === 0) {
files_uploaded.value = false
emit("files_uploaded", [])
}
}

async function upload_files() {
toggle_loading()
var promise_array = []
Expand Down Expand Up @@ -92,11 +127,6 @@
}
}

function clear() {
internal_files.value = []
emit("files_uploaded", internal_files.value)
}

watch(
() => props.files,
(newVal) => {
Expand All @@ -107,17 +137,8 @@

watch(internal_files, (value) => {
files_uploaded.value = false
if (props.auto_upload) {
if (props.multiple.value == false) {
internal_files.value = [value]
}
if (props.auto_upload && value.length > 0) {
upload_files()
}
})
</script>

<style scoped>
.div.v-input__details {
display: none;
}
</style>
2 changes: 1 addition & 1 deletion app/components/Launcher.vue
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
align-self="center"
style="z-index: 1000"
>
<Recaptcha :color="'secondary'" />
<Recaptcha :button_color="'secondary'" />
</v-col>
<v-col v-else-if="infraStore.status == Status.CREATING">
<Loading />
Expand Down
7 changes: 6 additions & 1 deletion app/components/Recaptcha.vue
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
<VCol cols="4" class="d-flex justify-center align-center">
<VBtn
:text="props.button_label"
:color="props.button_color"
:color="props.color || props.button_color"
@click="submit_recaptcha"
/>
</VCol>
Expand All @@ -53,11 +53,16 @@
required: false,
default: "white",
},
color: {
type: String,
required: false,
},
})
const infraStore = useInfraStore()
const name = ref("")
const email = ref("")
const launch = ref(false)
const valid = ref(false)
const emailRules = [
(value) => {
if (value) {
Expand Down
Loading
Loading