Files
oysqn.app/app/pages/index.vue

193 lines
6.4 KiB
Vue
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<IonPage>
<!-- Splash: unauthenticated -->
<template v-if="!user">
<IonContent class="splash-content">
<div class="splash-center">
<img src="/oysqn_logo.png" alt="OYS Borrow a Boat" class="splash-logo" />
<IonButton expand="block" router-link="/login" class="splash-btn">Log In</IonButton>
</div>
</IonContent>
</template>
<!-- Home: authenticated -->
<template v-else>
<IonHeader>
<IonToolbar color="primary">
<IonButtons slot="start">
<IonMenuButton />
</IonButtons>
<IonTitle>Home</IonTitle>
</IonToolbar>
</IonHeader>
<IonContent class="ion-padding">
<h2 class="welcome">Welcome, {{ auth.displayName }}</h2>
<IonCard>
<IonCardHeader>
<IonCardTitle>Upcoming Reservations</IonCardTitle>
</IonCardHeader>
<IonCardContent>
<div v-if="loadingReservations" class="ion-text-center ion-padding">
<IonSpinner name="crescent" />
</div>
<p v-else-if="upcomingReservations.length === 0" class="empty-text">
No upcoming reservations.
</p>
<IonList v-else lines="full">
<IonItem
v-for="r in upcomingReservations"
:key="r.id"
button
detail
@click="openActionSheet(r)"
>
<IonLabel>
<h3>{{ boatName(r) }}</h3>
<p>{{ formatDateRange(r.start_time, r.end_time) }}</p>
</IonLabel>
<IonBadge slot="end" :color="statusColor(r.status)">{{ r.status }}</IonBadge>
</IonItem>
</IonList>
</IonCardContent>
</IonCard>
<IonButton expand="block" router-link="/reservations/create" class="ion-margin-bottom">
<IonIcon slot="start" :icon="addCircleOutline" />
Create Reservation
</IonButton>
<IonCard>
<IonCardHeader>
<IonCardTitle>Calendar</IonCardTitle>
</IonCardHeader>
<IonCardContent class="calendar-content">
<DatePicker inline :min-date="today" />
</IonCardContent>
</IonCard>
<WeatherWidget />
</IonContent>
</template>
</IonPage>
</template>
<script setup lang="ts">
import {
IonPage, IonHeader, IonToolbar, IonTitle, IonContent,
IonButtons, IonMenuButton, IonButton, IonCard, IonCardHeader,
IonCardTitle, IonCardContent, IonList, IonItem, IonLabel,
IonBadge, IonSpinner, IonIcon, actionSheetController, onIonViewWillEnter,
} from '@ionic/vue'
import { addCircleOutline, createOutline, trashOutline } from 'ionicons/icons'
import { useAuthStore } from '~/stores/auth'
import type { Database, ReservationStatus } from '~/types/supabase'
type ReservationWithBoat = Database['public']['Tables']['reservations']['Row'] & {
boats: { name: string; display_name: string | null } | null
}
definePageMeta({ layout: false })
const user = useSupabaseUser()
const auth = useAuthStore()
const supabase = useSupabaseClient<Database>()
const router = useRouter()
const today = new Date()
const loadingReservations = ref(true)
const upcomingReservations = ref<ReservationWithBoat[]>([])
async function fetchReservations() {
if (!user.value) return
loadingReservations.value = true
const { data } = await (supabase as ReturnType<typeof useSupabaseClient>)
.from('reservations')
.select('*, boats(name, display_name)')
.eq('user_id', user.value.id)
.gte('start_time', new Date().toISOString())
.neq('status', 'cancelled')
.order('start_time', { ascending: true })
.limit(3)
upcomingReservations.value = (data as ReservationWithBoat[]) ?? []
loadingReservations.value = false
}
// Initial load and re-fetch on every page visit (Ionic caches the page, so
// watch(user) alone won't re-run when navigating back from create/edit).
watch(user, (val) => { if (val) fetchReservations() }, { immediate: true })
onIonViewWillEnter(() => { if (user.value) fetchReservations() })
async function openActionSheet(r: ReservationWithBoat) {
const isFuture = new Date(r.start_time) > new Date()
const sheet = await actionSheetController.create({
header: `${boatName(r)}${formatDateRange(r.start_time, r.end_time)}`,
buttons: [
...(isFuture ? [
{
text: 'Edit',
icon: createOutline,
handler: () => void router.push(`/reservations/edit/${r.id}`),
},
{
text: 'Cancel Reservation',
role: 'destructive',
icon: trashOutline,
handler: () => void cancelReservation(r.id),
},
] : []),
{ text: 'Dismiss', role: 'cancel' },
],
})
await sheet.present()
}
async function cancelReservation(id: string) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
await (supabase as any).from('reservations').update({ status: 'cancelled' }).eq('id', id)
upcomingReservations.value = upcomingReservations.value.filter(r => r.id !== id)
}
function boatName(r: ReservationWithBoat) {
return r.boats?.display_name || r.boats?.name || 'Unknown boat'
}
function formatDateRange(start: string, end: string) {
const s = new Date(start)
const e = new Date(end)
const date = s.toLocaleDateString('en-CA', { weekday: 'short', month: 'short', day: 'numeric' })
const startTime = s.toLocaleTimeString('en-CA', { hour: '2-digit', minute: '2-digit' })
const endTime = e.toLocaleTimeString('en-CA', { hour: '2-digit', minute: '2-digit' })
return `${date} · ${startTime}${endTime}`
}
function statusColor(status: ReservationStatus): string {
const colors: Record<ReservationStatus, string> = {
pending: 'warning',
tentative: 'medium',
confirmed: 'success',
cancelled: 'danger',
}
return colors[status]
}
</script>
<style scoped>
.splash-content { --background: var(--ion-color-light); }
.splash-center {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
padding: 2rem;
gap: 2rem;
}
.splash-logo { max-width: 280px; width: 100%; }
.splash-btn { width: 100%; max-width: 280px; }
.welcome { font-size: 1.25rem; font-weight: 600; margin: 0 0 1rem; }
.empty-text { color: var(--ion-color-medium); margin: 0; }
.calendar-content { display: flex; justify-content: center; padding: 0.5rem 0; }
</style>