Skip to content

Commit 951b4de

Browse files
committed
added rides page, create rides, edit rides, and google maps
1 parent cb18c08 commit 951b4de

File tree

10 files changed

+575
-65
lines changed

10 files changed

+575
-65
lines changed

.env.example

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,4 @@ BETTER_AUTH_SECRET=
33
BETTER_AUTH_URL=http://localhost:3000
44
EMAIL_USER=
55
EMAIL_PASS=""
6+
NUXT_PUBLIC_GOOGLE_MAPS_API_KEY=

app/pages/rides.vue

Lines changed: 0 additions & 65 deletions
This file was deleted.

app/pages/rides/[id].vue

Lines changed: 259 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,259 @@
1+
<script setup lang="ts">
2+
import * as z from 'zod'
3+
4+
const route = useRoute()
5+
const id = route.params.id
6+
const toast = useToast()
7+
8+
const { data: ride, status, refresh: refreshRide } = await useFetch(`/api/get/rides/byId/${id}`)
9+
10+
const isEditModalOpen = ref(false)
11+
const isDeleteModalOpen = ref(false)
12+
13+
const schema = z.object({
14+
pickupDisplay: z.string().min(1, 'Pickup address is required'),
15+
dropoffDisplay: z.string().min(1, 'Dropoff address is required'),
16+
scheduledTime: z.string().min(1, 'Scheduled time is required'),
17+
notes: z.string().optional(),
18+
})
19+
20+
const editState = reactive({
21+
pickupDisplay: '',
22+
dropoffDisplay: '',
23+
scheduledTime: '',
24+
notes: '',
25+
})
26+
27+
// Initialize edit state when ride is loaded or modal opens
28+
watch(isEditModalOpen, (val) => {
29+
if (val && ride.value) {
30+
editState.pickupDisplay = ride.value.pickupDisplay
31+
editState.dropoffDisplay = ride.value.dropoffDisplay
32+
// Format date for datetime-local input (YYYY-MM-DDTHH:mm)
33+
const date = new Date(ride.value.scheduledTime)
34+
editState.scheduledTime = new Date(date.getTime() - date.getTimezoneOffset() * 60000)
35+
.toISOString()
36+
.slice(0, 16)
37+
editState.notes = ride.value.notes || ''
38+
}
39+
})
40+
41+
async function handleUpdate(event: any) {
42+
try {
43+
await $fetch(`/api/put/rides/${id}`, {
44+
method: 'PUT',
45+
body: event.data,
46+
})
47+
toast.add({ title: 'Success', description: 'Ride updated successfully', color: 'success' })
48+
isEditModalOpen.value = false
49+
await refreshRide()
50+
} catch (err) {
51+
toast.add({ title: 'Error', description: 'Failed to update ride', color: 'error' })
52+
}
53+
}
54+
55+
async function handleDelete() {
56+
try {
57+
await $fetch(`/api/delete/rides/${id}`, {
58+
method: 'DELETE',
59+
})
60+
toast.add({ title: 'Success', description: 'Ride deleted successfully', color: 'success' })
61+
await navigateTo('/rides')
62+
} catch (err) {
63+
toast.add({ title: 'Error', description: 'Failed to delete ride', color: 'error' })
64+
}
65+
}
66+
67+
const mapUrl = computed(() => {
68+
if (!ride.value) return ''
69+
const origin = encodeURIComponent(ride.value.pickupDisplay)
70+
const destination = encodeURIComponent(ride.value.dropoffDisplay)
71+
const apiKey = useRuntimeConfig().public.googleMapsApiKey
72+
return `https://www.google.com/maps/embed/v1/directions?key=${apiKey || ''}&origin=${origin}&destination=${destination}`
73+
})
74+
75+
const breadcrumbs = [{ label: 'Rides', to: '/rides' }, { label: 'Ride Details' }]
76+
</script>
77+
78+
<template>
79+
<UContainer class="py-10">
80+
<UBreadcrumb :items="breadcrumbs" class="mb-6" />
81+
82+
<div v-if="status === 'pending'" class="flex h-64 items-center justify-center">
83+
<USkeleton class="h-64 w-full" />
84+
</div>
85+
86+
<div v-else-if="!ride" class="py-20 text-center">
87+
<h1 class="mb-4 text-2xl font-bold">Ride not found</h1>
88+
<UButton to="/rides" label="Back to Rides" color="neutral" variant="ghost" />
89+
</div>
90+
91+
<div v-else class="grid grid-cols-1 gap-8 lg:grid-cols-3">
92+
<!-- Left Column: Details -->
93+
<div class="space-y-6 lg:col-span-1">
94+
<UCard>
95+
<template #header>
96+
<div class="flex items-center justify-between">
97+
<h2 class="text-xl font-bold">Ride Information</h2>
98+
<UBadge
99+
:color="
100+
ride.status === 'COMPLETED'
101+
? 'success'
102+
: ride.status === 'CANCELLED'
103+
? 'error'
104+
: 'info'
105+
"
106+
variant="subtle"
107+
>
108+
{{ ride.status }}
109+
</UBadge>
110+
</div>
111+
</template>
112+
113+
<div class="space-y-4">
114+
<div>
115+
<p class="text-sm text-gray-500">Scheduled Time</p>
116+
<p class="font-medium">
117+
{{ new Date(ride.scheduledTime).toLocaleString() }}
118+
</p>
119+
</div>
120+
121+
<div>
122+
<p class="text-sm text-gray-500">Client</p>
123+
<p class="font-medium">{{ ride.client?.user?.name }}</p>
124+
<p class="text-sm text-gray-500">{{ ride.client?.user?.email }}</p>
125+
</div>
126+
127+
<div>
128+
<p class="text-sm text-gray-500">Volunteer</p>
129+
<p class="font-medium" v-if="ride.volunteer">
130+
{{ ride.volunteer?.user?.name }}
131+
</p>
132+
<p class="text-gray-400 italic" v-else>No volunteer assigned</p>
133+
</div>
134+
135+
<div v-if="ride.notes">
136+
<p class="text-sm text-gray-500">Notes</p>
137+
<p class="rounded border bg-gray-50 p-3 text-sm">{{ ride.notes }}</p>
138+
</div>
139+
</div>
140+
141+
<template #footer>
142+
<div class="flex gap-2">
143+
<UButton
144+
label="Edit"
145+
icon="i-lucide-edit"
146+
variant="subtle"
147+
class="flex-1 justify-center"
148+
@click="isEditModalOpen = true"
149+
/>
150+
<UButton
151+
label="Delete"
152+
icon="i-lucide-trash"
153+
color="error"
154+
variant="subtle"
155+
@click="isDeleteModalOpen = true"
156+
/>
157+
</div>
158+
</template>
159+
</UCard>
160+
</div>
161+
162+
<!-- Right Column: Map and Route -->
163+
<div class="space-y-6 lg:col-span-2">
164+
<UCard>
165+
<template #header>
166+
<h2 class="text-xl font-bold">Route</h2>
167+
</template>
168+
169+
<div class="space-y-4">
170+
<div class="flex items-start gap-3">
171+
<UIcon name="i-lucide-map-pin" class="text-primary mt-1 size-5" />
172+
<div>
173+
<p class="text-sm text-gray-500">Pickup</p>
174+
<p class="font-medium">{{ ride.pickupDisplay }}</p>
175+
</div>
176+
</div>
177+
178+
<div class="flex items-start gap-3">
179+
<UIcon name="i-lucide-flag" class="text-error mt-1 size-5" />
180+
<div>
181+
<p class="text-sm text-gray-500">Dropoff</p>
182+
<p class="font-medium">{{ ride.dropoffDisplay }}</p>
183+
</div>
184+
</div>
185+
186+
<div class="aspect-video w-full overflow-hidden rounded-lg border">
187+
<iframe
188+
v-if="mapUrl && useRuntimeConfig().public.googleMapsApiKey"
189+
width="100%"
190+
height="100%"
191+
frameborder="0"
192+
style="border: 0"
193+
:src="mapUrl"
194+
allowfullscreen
195+
></iframe>
196+
<div
197+
v-else
198+
class="flex h-full items-center justify-center bg-gray-100 text-gray-400 italic"
199+
>
200+
Map API Key missing or invalid address
201+
</div>
202+
</div>
203+
</div>
204+
</UCard>
205+
</div>
206+
</div>
207+
208+
<!-- Edit Modal -->
209+
<UModal v-model:open="isEditModalOpen" title="Edit Ride">
210+
<template #content>
211+
<UForm :schema="schema" :state="editState" class="space-y-4 p-4" @submit="handleUpdate">
212+
<UFormField label="Pickup Address" name="pickupDisplay">
213+
<UInput v-model="editState.pickupDisplay" class="w-full" />
214+
</UFormField>
215+
216+
<UFormField label="Dropoff Address" name="dropoffDisplay">
217+
<UInput v-model="editState.dropoffDisplay" class="w-full" />
218+
</UFormField>
219+
220+
<UFormField label="Scheduled Time" name="scheduledTime">
221+
<UInput v-model="editState.scheduledTime" type="datetime-local" class="w-full" />
222+
</UFormField>
223+
224+
<UFormField label="Notes" name="notes">
225+
<UTextarea v-model="editState.notes" class="w-full" />
226+
</UFormField>
227+
228+
<div class="flex justify-end gap-2 pt-4">
229+
<UButton
230+
label="Cancel"
231+
color="neutral"
232+
variant="ghost"
233+
@click="isEditModalOpen = false"
234+
/>
235+
<UButton type="submit" label="Save Changes" color="primary" />
236+
</div>
237+
</UForm>
238+
</template>
239+
</UModal>
240+
241+
<!-- Delete Confirmation Modal -->
242+
<UModal v-model:open="isDeleteModalOpen" title="Delete Ride">
243+
<template #content>
244+
<div class="space-y-4 p-4">
245+
<p>Are you sure you want to delete this ride? This action cannot be undone.</p>
246+
<div class="flex justify-end gap-2">
247+
<UButton
248+
label="Cancel"
249+
color="neutral"
250+
variant="ghost"
251+
@click="isDeleteModalOpen = false"
252+
/>
253+
<UButton label="Delete" color="error" @click="handleDelete" />
254+
</div>
255+
</div>
256+
</template>
257+
</UModal>
258+
</UContainer>
259+
</template>

0 commit comments

Comments
 (0)