279 lines
9.5 KiB
Vue
279 lines
9.5 KiB
Vue
<script setup>
|
|
import { Button } from '@/components/ui/button/index.js'
|
|
import { useForm } from 'vee-validate'
|
|
import { FormControl, FormField, FormItem, FormLabel } from '@/components/ui/form/index.js'
|
|
import { Textarea } from '@/components/ui/textarea/index.js'
|
|
import { Input } from '@/components/ui/input/index.js'
|
|
import { Label } from '@/components/ui/label/index.js'
|
|
import { toTypedSchema } from '@vee-validate/zod'
|
|
import { z } from 'zod'
|
|
import { onMounted, onUnmounted, ref } from 'vue'
|
|
import { AlertCircle, ArrowLeft, ArrowRight, CircleCheckBig, CirclePlus, RefreshCcw } from 'lucide-vue-next'
|
|
import {
|
|
Carousel,
|
|
CarouselContent,
|
|
CarouselItem,
|
|
CarouselNext,
|
|
CarouselPrevious
|
|
} from '@/components/ui/carousel/index.js'
|
|
import { Card, CardContent } from '@/components/ui/card/index.js'
|
|
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert/index.js'
|
|
import { useAuthStore } from '@/stores/auth.js'
|
|
import { API_BASE_URL, S3_BUCKET, S3_ENDPOINT } from '@/config.js'
|
|
|
|
const authStore = useAuthStore()
|
|
|
|
const formSchema = toTypedSchema(z.object({
|
|
description: z.string().min(1),
|
|
latitude: z.number(),
|
|
longitude: z.number(),
|
|
city: z.string().min(1),
|
|
country: z.string().min(1)
|
|
}))
|
|
const form = useForm({
|
|
validationSchema: formSchema
|
|
})
|
|
|
|
const formContainer = ref(null)
|
|
const latitudeInput = ref(null)
|
|
const longitudeInput = ref(null)
|
|
const selectedFiles = ref([])
|
|
|
|
const formStatus = ref({
|
|
sending: false,
|
|
sent: false,
|
|
error: false,
|
|
errorMsg: ''
|
|
})
|
|
|
|
const onSubmit = form.handleSubmit(async (values) => {
|
|
if (!authStore.isAuth)
|
|
return
|
|
console.log('Envoi du post...')
|
|
if (selectedFiles.value.length === 0)
|
|
return
|
|
formContainer.value.classList.add('hidden')
|
|
formStatus.value.sending = true
|
|
|
|
const assets = []
|
|
for (const file of selectedFiles.value) {
|
|
console.log('Contact API asset')
|
|
const response = await fetch(API_BASE_URL + '/admin/assets', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'X-admin-token': authStore.adminToken
|
|
},
|
|
body: JSON.stringify({ filename: file.file.name })
|
|
})
|
|
|
|
if (!response.ok) {
|
|
console.log('Contact API asset failed: ' + response.statusText + '\n\n' + response.body)
|
|
formStatus.value.sending = false
|
|
formStatus.value.error = true
|
|
formStatus.value.errorMsg = 'Une erreur est survenue à la préparation de l\'envoi d\'un média : ' + response.statusText
|
|
formContainer.value.classList.remove('invisible')
|
|
return
|
|
}
|
|
const responseBody = await response.json()
|
|
const mediaUploadFormData = new FormData()
|
|
for (const [key, value] of Object.entries(responseBody.formData)) {
|
|
mediaUploadFormData.append(key, value)
|
|
}
|
|
mediaUploadFormData.append('key', '${filename}')
|
|
mediaUploadFormData.append('Content-Type', file.file.type)
|
|
mediaUploadFormData.append('file', file.file, responseBody.filename)
|
|
|
|
console.log('Envoi image sur s3')
|
|
const s3Response = await fetch(`${S3_ENDPOINT}/${S3_BUCKET}/`, {
|
|
method: 'POST',
|
|
body: mediaUploadFormData
|
|
})
|
|
if (!s3Response.ok) {
|
|
console.log('Envoi media S3 failed: ' + s3Response.statusText + '\n\n' + s3Response.body)
|
|
formStatus.value.sending = false
|
|
formStatus.value.error = true
|
|
formStatus.value.errorMsg = 'Une erreur est survenue pendant l\'envoi d\'un média : ' + s3Response.statusText
|
|
formContainer.value.classList.remove('invisible')
|
|
return
|
|
}
|
|
|
|
assets.push(responseBody.id)
|
|
}
|
|
|
|
const response = await fetch(API_BASE_URL + '/admin/posts', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'X-admin-token': authStore.adminToken
|
|
},
|
|
body: JSON.stringify({
|
|
description: values.description,
|
|
latitude: values.latitude,
|
|
longitude: values.longitude,
|
|
city: values.city,
|
|
country: values.country,
|
|
assets: assets
|
|
})
|
|
})
|
|
if (!response.ok) {
|
|
console.log('POST post API failed: ' + response.statusText + '\n\n' + response.body)
|
|
formStatus.value.sending = false
|
|
formStatus.value.error = true
|
|
formStatus.value.errorMsg = 'Une erreur est survenue lors de l\'envoi du poste : ' + response.statusText
|
|
formContainer.value.classList.remove('hidden')
|
|
return
|
|
}
|
|
|
|
formStatus.value.sending = false
|
|
formStatus.value.sent = true
|
|
})
|
|
|
|
let geoWatchId = null
|
|
|
|
onMounted(() => {
|
|
geoWatchId = navigator.geolocation.watchPosition((position) => {
|
|
console.log(position)
|
|
form.setFieldValue('latitude', position.coords.latitude)
|
|
form.setFieldValue('longitude', position.coords.longitude)
|
|
})
|
|
})
|
|
|
|
onUnmounted(() => {
|
|
if (geoWatchId != null) {
|
|
navigator.geolocation.clearWatch(geoWatchId)
|
|
}
|
|
})
|
|
|
|
function onFilesSelected(event) {
|
|
console.log(event.target.files)
|
|
selectedFiles.value = []
|
|
for (const file of event.target.files) {
|
|
selectedFiles.value.push({ displayUrl: URL.createObjectURL(file), file: file })
|
|
}
|
|
}
|
|
|
|
function mediaReorder(index, diff) {
|
|
const tmp = selectedFiles.value[index]
|
|
selectedFiles.value[index] = selectedFiles.value[index + diff]
|
|
selectedFiles.value[index + diff] = tmp
|
|
}
|
|
|
|
</script>
|
|
|
|
<template>
|
|
<div class="grid grid-cols-3 grid-rows-1 mt-20 w-full justify-center items-center gap-4">
|
|
<div class="col-span-3 col-start-1 lg:col-span-1 lg:col-start-2 mx-5 lg:mx-0">
|
|
<h1 class="scroll-m-20 text-4xl font-extrabold tracking-tight lg:text-5xl mb-6">
|
|
Ajouter un post
|
|
</h1>
|
|
<RouterLink to="/admin">
|
|
<Button class="mb-6">
|
|
<ArrowLeft class="mr-2" />
|
|
Retour
|
|
</Button>
|
|
</RouterLink>
|
|
<Alert variant="destructive" class="mb-6" v-if="formStatus.error">
|
|
<AlertCircle class="w-4 h-4"></AlertCircle>
|
|
<AlertTitle>Erreur...</AlertTitle>
|
|
<AlertDescription>{{ formStatus.errorMsg }}</AlertDescription>
|
|
</Alert>
|
|
<Alert class="mb-6" v-if="formStatus.sent">
|
|
<CircleCheckBig class="w-4 h-4"></CircleCheckBig>
|
|
<AlertTitle>Post créé !</AlertTitle>
|
|
</Alert>
|
|
<p v-if="formStatus.sending">Sending...</p>
|
|
<div id="form-container" ref="formContainer">
|
|
<form id="post-form" class="space-y-6" @submit="onSubmit">
|
|
<FormField v-slot="{ componentField }" name="description">
|
|
<FormItem>
|
|
<FormLabel>Description</FormLabel>
|
|
<FormControl>
|
|
<Textarea v-bind="componentField"></Textarea>
|
|
</FormControl>
|
|
</FormItem>
|
|
</FormField>
|
|
<div class="grid grid-cols-2 gap-4">
|
|
<FormField ref="latitudeInput" v-slot="{ componentField }" name="latitude">
|
|
<FormItem>
|
|
<FormLabel>Latitude</FormLabel>
|
|
<FormControl>
|
|
<Input disabled v-bind="componentField"></Input>
|
|
</FormControl>
|
|
</FormItem>
|
|
</FormField>
|
|
<FormField ref="longitudeInput" v-slot="{ componentField }" name="longitude">
|
|
<FormItem>
|
|
<FormLabel>Longitude</FormLabel>
|
|
<FormControl>
|
|
<Input disabled v-bind="componentField"></Input>
|
|
</FormControl>
|
|
</FormItem>
|
|
</FormField>
|
|
</div>
|
|
<div class="grid grid-cols-9 gap-4">
|
|
<FormField v-slot="{ componentField }" name="city">
|
|
<FormItem class="col-span-4">
|
|
<FormLabel>Ville</FormLabel>
|
|
<FormControl>
|
|
<Input v-bind="componentField"></Input>
|
|
</FormControl>
|
|
</FormItem>
|
|
</FormField>
|
|
<FormField v-slot="{ componentField }" name="country">
|
|
<FormItem class="col-span-4">
|
|
<FormLabel>Pays</FormLabel>
|
|
<FormControl>
|
|
<Input v-bind="componentField"></Input>
|
|
</FormControl>
|
|
</FormItem>
|
|
</FormField>
|
|
<Button class="place-self-end">
|
|
<RefreshCcw />
|
|
</Button>
|
|
</div>
|
|
</form>
|
|
<form id="assets-form" class="mt-6">
|
|
<Label for="medias">Medias</Label>
|
|
<Input id="medias" type="file" accept="image/*,video/*" multiple @change="onFilesSelected" />
|
|
</form>
|
|
<Carousel
|
|
class="w-full mt-10"
|
|
:opts="{
|
|
align: 'start',
|
|
}"
|
|
v-if="selectedFiles.length > 0"
|
|
>
|
|
<CarouselContent>
|
|
<CarouselItem v-for="(file, index) in selectedFiles" :key="index" class="md:basis-1/2 lg:basis-1/3">
|
|
<div class="flex flex-col">
|
|
<Card>
|
|
<CardContent class="flex aspect-square items-center justify-center p-6">
|
|
<img :src="file.displayUrl" v-if="file.file.type.startsWith('image/')" />
|
|
</CardContent>
|
|
</Card>
|
|
<div class="grid grid-cols-2 justify-items-center mt-2">
|
|
<Button v-if="index > 0" @click="mediaReorder(index, -1)">
|
|
<ArrowLeft />
|
|
</Button>
|
|
<div v-else></div>
|
|
<Button v-if="index < selectedFiles.length - 1" @click="mediaReorder(index, 1)">
|
|
<ArrowRight />
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</CarouselItem>
|
|
</CarouselContent>
|
|
<CarouselPrevious />
|
|
<CarouselNext />
|
|
</Carousel>
|
|
<Button class="mt-6 w-full" type="submit" form="post-form">
|
|
<CirclePlus />
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<style scoped>
|
|
</style>
|