test: Add E2E test framework with Playwright

feat: Basic Homepage elements
This commit is contained in:
2026-04-20 07:11:56 -04:00
parent 5c830443f3
commit d07a02c9dc
13 changed files with 625 additions and 32 deletions

View File

@@ -21,7 +21,46 @@
</IonToolbar>
</IonHeader>
<IonContent class="ion-padding">
<h2>Welcome to OYS Borrow a Boat</h2>
<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">
<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>
@@ -30,19 +69,69 @@
<script setup lang="ts">
import {
IonPage, IonHeader, IonToolbar, IonTitle, IonContent,
IonButtons, IonMenuButton, IonButton,
IonButtons, IonMenuButton, IonButton, IonCard, IonCardHeader,
IonCardTitle, IonCardContent, IonList, IonItem, IonLabel,
IonBadge, IonSpinner, IonIcon,
} from '@ionic/vue'
import { addCircleOutline } from 'ionicons/icons'
import { useAuthStore } from '~/stores/auth'
import type { Database, ReservationStatus } from '~/types/supabase'
const user = useSupabaseUser()
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 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())
.order('start_time', { ascending: true })
.limit(3)
upcomingReservations.value = (data as ReservationWithBoat[]) ?? []
loadingReservations.value = false
}
watch(user, (val) => { if (val) fetchReservations() }, { immediate: true })
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',
}
return colors[status]
}
</script>
<style scoped>
.splash-content {
--background: var(--ion-color-light);
}
.splash-content { --background: var(--ion-color-light); }
.splash-center {
display: flex;
flex-direction: column;
@@ -52,14 +141,10 @@ definePageMeta({ layout: false })
padding: 2rem;
gap: 2rem;
}
.splash-logo { max-width: 280px; width: 100%; }
.splash-btn { width: 100%; max-width: 280px; }
.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>