add location logging, display last location on map
Signed-off-by: Nicolas Froger <nicolas@kektus.xyz>
This commit is contained in:
parent
4356816f07
commit
130581a411
13 changed files with 386 additions and 3 deletions
|
|
@ -6,6 +6,7 @@ const CreatePostView = () => import('@/views/CreatePostView.vue')
|
|||
const AdminView = () => import('@/views/AdminView.vue')
|
||||
const LoginView = () => import('@/views/LoginView.vue')
|
||||
const MapView = () => import('@/views/MapView.vue')
|
||||
const SendLocationView = () => import('@/views/SendLocationView.vue')
|
||||
|
||||
function authenticatedRoute(to) {
|
||||
const authStore = useAuthStore()
|
||||
|
|
@ -46,6 +47,12 @@ const router = createRouter({
|
|||
name: 'admin_create_post',
|
||||
component: CreatePostView,
|
||||
beforeEnter: [authenticatedRoute]
|
||||
},
|
||||
{
|
||||
path: '/admin/send-location',
|
||||
name: 'admin_send_location',
|
||||
component: SendLocationView,
|
||||
beforeEnter: [authenticatedRoute]
|
||||
}
|
||||
]
|
||||
})
|
||||
|
|
|
|||
32
summer2024-frontend/src/stores/location.js
Normal file
32
summer2024-frontend/src/stores/location.js
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
import { defineStore } from 'pinia'
|
||||
import { ref } from 'vue'
|
||||
import { formatRelative } from 'date-fns'
|
||||
import { fr } from 'date-fns/locale'
|
||||
import { API_BASE_URL } from '@/config.js'
|
||||
import { fromLonLat } from 'ol/proj.js'
|
||||
|
||||
export const useLocationStore = defineStore('location', () => {
|
||||
const locationApiPath = API_BASE_URL + '/location/last'
|
||||
|
||||
const lastLocation = ref(null)
|
||||
|
||||
function fetchLocation() {
|
||||
return fetch(locationApiPath)
|
||||
.then((response) => {
|
||||
return response.json()
|
||||
})
|
||||
.then(async (loc) => {
|
||||
const locDate = new Date(loc.timestamp)
|
||||
loc.formatedDate = formatRelative(locDate, new Date(), { locale: fr })
|
||||
loc.projectedCoordinates = fromLonLat([loc.longitude, loc.latitude])
|
||||
lastLocation.value = loc
|
||||
|
||||
return location.value
|
||||
})
|
||||
.catch((error) => {
|
||||
console.log('fetch last location failed with: ' + error)
|
||||
})
|
||||
}
|
||||
|
||||
return { lastLocation, fetchLocation }
|
||||
})
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
<script setup>
|
||||
|
||||
import { Button } from '@/components/ui/button/index.js'
|
||||
import { CirclePlus, Pencil, Trash } from 'lucide-vue-next'
|
||||
import { CirclePlus, Pencil, Trash, MapPin } from 'lucide-vue-next'
|
||||
import { useAdminPostsStore } from '@/stores/adminPosts.js'
|
||||
import { onMounted } from 'vue'
|
||||
import {
|
||||
|
|
@ -27,6 +27,12 @@ onMounted(() => {
|
|||
<h1 class="scroll-m-20 text-4xl font-extrabold tracking-tight lg:text-5xl mb-6">
|
||||
Admin
|
||||
</h1>
|
||||
<RouterLink to="/admin/send-location">
|
||||
<Button class="mb-6 mr-2">
|
||||
<MapPin class="mr-2" />
|
||||
Envoyer localisation
|
||||
</Button>
|
||||
</RouterLink>
|
||||
<RouterLink to="/admin/post/create">
|
||||
<Button class="mb-6">
|
||||
<CirclePlus class="mr-2" />
|
||||
|
|
|
|||
|
|
@ -2,13 +2,18 @@
|
|||
|
||||
import { usePostsStore } from '@/stores/posts.js'
|
||||
import { computed, onMounted } from 'vue'
|
||||
import { Camera } from 'lucide-vue-next'
|
||||
import { Camera, MapPin } from 'lucide-vue-next'
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip/index.js'
|
||||
import { useLocationStore } from '@/stores/location.js'
|
||||
|
||||
const postStore = usePostsStore()
|
||||
const locationStore = useLocationStore()
|
||||
|
||||
const postsLinesCoordinates = computed(() => {
|
||||
const coords = []
|
||||
if (locationStore.lastLocation != null) {
|
||||
coords.push(locationStore.lastLocation.projectedCoordinates)
|
||||
}
|
||||
for (const post of postStore.posts) {
|
||||
coords.push(post.projectedCoordinates)
|
||||
}
|
||||
|
|
@ -17,6 +22,7 @@ const postsLinesCoordinates = computed(() => {
|
|||
|
||||
onMounted(() => {
|
||||
postStore.fetchPosts()
|
||||
locationStore.fetchLocation()
|
||||
})
|
||||
</script>
|
||||
|
||||
|
|
@ -59,8 +65,27 @@ onMounted(() => {
|
|||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</ol-overlay>
|
||||
<ol-overlay
|
||||
v-if="locationStore.lastLocation != null"
|
||||
:position="locationStore.lastLocation.projectedCoordinates"
|
||||
:autoPan="true"
|
||||
>
|
||||
<TooltipProvider>
|
||||
<Tooltip :default-open="true">
|
||||
<TooltipTrigger as-child>
|
||||
<div
|
||||
class="-translate-x-1/2 -translate-y-1/2 p-1 rounded-lg bg-rose-950 hover:bg-rose-900 transition-colors duration-200">
|
||||
<MapPin size="16" />
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>Dernière position, {{ locationStore.lastLocation.formatedDate }}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</ol-overlay>
|
||||
|
||||
<ol-vector-layer v-if="postStore.posts.length > 1">
|
||||
<ol-vector-layer v-if="postsLinesCoordinates.length > 1">
|
||||
<ol-source-vector>
|
||||
<ol-feature ref="profileFeatureRef">
|
||||
<ol-geom-line-string
|
||||
|
|
|
|||
137
summer2024-frontend/src/views/SendLocationView.vue
Normal file
137
summer2024-frontend/src/views/SendLocationView.vue
Normal file
|
|
@ -0,0 +1,137 @@
|
|||
<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 { Input } from '@/components/ui/input/index.js'
|
||||
import { toTypedSchema } from '@vee-validate/zod'
|
||||
import { z } from 'zod'
|
||||
import { onMounted, onUnmounted, ref } from 'vue'
|
||||
import { AlertCircle, ArrowLeft, CircleCheckBig, Send } from 'lucide-vue-next'
|
||||
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert/index.js'
|
||||
import { useAuthStore } from '@/stores/auth.js'
|
||||
import { API_BASE_URL } from '@/config.js'
|
||||
|
||||
const authStore = useAuthStore()
|
||||
|
||||
const formSchema = toTypedSchema(z.object({
|
||||
latitude: z.number(),
|
||||
longitude: z.number()
|
||||
}))
|
||||
const form = useForm({
|
||||
validationSchema: formSchema
|
||||
})
|
||||
|
||||
const formContainer = ref(null)
|
||||
const latitudeInput = ref(null)
|
||||
const longitudeInput = ref(null)
|
||||
|
||||
const formStatus = ref({
|
||||
sending: false,
|
||||
sent: false,
|
||||
error: false,
|
||||
errorMsg: ''
|
||||
})
|
||||
|
||||
const onSubmit = form.handleSubmit(async (values) => {
|
||||
if (!authStore.isAuth)
|
||||
return
|
||||
|
||||
console.log('Envoi de la localisation...')
|
||||
formContainer.value.classList.add('hidden')
|
||||
formStatus.value.sending = true
|
||||
|
||||
const response = await fetch(API_BASE_URL + '/admin/location', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-admin-token': authStore.adminToken
|
||||
},
|
||||
body: JSON.stringify({
|
||||
latitude: values.latitude,
|
||||
longitude: values.longitude
|
||||
})
|
||||
})
|
||||
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)
|
||||
}
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="grid grid-cols-3 grid-rows-1 mt-28 sm: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">
|
||||
Envoyer la localisation
|
||||
</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>Localisation envoyée !</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">
|
||||
<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>
|
||||
<Button class="mt-6 w-full" type="submit">
|
||||
<Send />
|
||||
</Button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
</style>
|
||||
Loading…
Add table
Add a link
Reference in a new issue