feat: implement basic auth workflow
This commit is contained in:
@@ -1,22 +1,65 @@
|
||||
<template>
|
||||
<IonPage>
|
||||
<IonHeader>
|
||||
<IonToolbar color="primary">
|
||||
<IonButtons slot="start">
|
||||
<IonMenuButton />
|
||||
</IonButtons>
|
||||
<IonTitle>Home</IonTitle>
|
||||
</IonToolbar>
|
||||
</IonHeader>
|
||||
<IonContent class="ion-padding">
|
||||
<h2>Welcome to OYS Borrow a Boat</h2>
|
||||
</IonContent>
|
||||
<!-- 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>Welcome to OYS Borrow a Boat</h2>
|
||||
</IonContent>
|
||||
</template>
|
||||
</IonPage>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
IonPage, IonHeader, IonToolbar, IonTitle, IonContent,
|
||||
IonButtons, IonMenuButton,
|
||||
IonButtons, IonMenuButton, IonButton,
|
||||
} from '@ionic/vue'
|
||||
|
||||
const user = useSupabaseUser()
|
||||
|
||||
definePageMeta({ layout: false })
|
||||
</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;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -2,17 +2,114 @@
|
||||
<IonPage>
|
||||
<IonHeader>
|
||||
<IonToolbar color="primary">
|
||||
<IonButtons slot="start">
|
||||
<IonBackButton default-href="/" />
|
||||
</IonButtons>
|
||||
<IonTitle>Sign In</IonTitle>
|
||||
</IonToolbar>
|
||||
</IonHeader>
|
||||
<IonContent class="ion-padding">
|
||||
<!-- TODO: Auth form -->
|
||||
<div class="login-form">
|
||||
<template v-if="!sent">
|
||||
<p class="ion-text-center">Enter your email address and we'll send you a sign-in link.</p>
|
||||
<IonList>
|
||||
<IonItem>
|
||||
<IonLabel position="stacked">Email address</IonLabel>
|
||||
<IonInput
|
||||
v-model="email"
|
||||
type="email"
|
||||
placeholder="you@example.com"
|
||||
autocomplete="email"
|
||||
inputmode="email"
|
||||
@keyup.enter="send"
|
||||
/>
|
||||
</IonItem>
|
||||
</IonList>
|
||||
<IonButton
|
||||
expand="block"
|
||||
class="ion-margin-top"
|
||||
:disabled="!email || loading"
|
||||
@click="send"
|
||||
>
|
||||
<IonSpinner v-if="loading" name="crescent" slot="start" />
|
||||
Send Sign-In Link
|
||||
</IonButton>
|
||||
<p v-if="error" class="error-text ion-text-center">{{ error }}</p>
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<div class="sent-state ion-text-center">
|
||||
<IonIcon :icon="mailOutline" class="sent-icon" />
|
||||
<h2>Check your email</h2>
|
||||
<p>A sign-in link was sent to <strong>{{ email }}</strong>. Open it on this device to sign in.</p>
|
||||
<IonButton fill="outline" expand="block" class="ion-margin-top" @click="reset">
|
||||
Use a different email
|
||||
</IonButton>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</IonContent>
|
||||
</IonPage>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { IonPage, IonHeader, IonToolbar, IonTitle, IonContent } from '@ionic/vue'
|
||||
import {
|
||||
IonPage, IonHeader, IonToolbar, IonTitle, IonContent,
|
||||
IonButtons, IonBackButton, IonButton, IonList, IonItem,
|
||||
IonLabel, IonInput, IonSpinner, IonIcon,
|
||||
} from '@ionic/vue'
|
||||
import { mailOutline } from 'ionicons/icons'
|
||||
import { useAuthStore } from '~/stores/auth'
|
||||
|
||||
definePageMeta({ layout: false })
|
||||
|
||||
const auth = useAuthStore()
|
||||
|
||||
const email = ref('')
|
||||
const loading = ref(false)
|
||||
const sent = ref(false)
|
||||
const error = ref('')
|
||||
|
||||
async function send() {
|
||||
if (!email.value || loading.value) return
|
||||
loading.value = true
|
||||
error.value = ''
|
||||
try {
|
||||
await auth.sendMagicLink(email.value.trim())
|
||||
sent.value = true
|
||||
} catch (e: unknown) {
|
||||
error.value = e instanceof Error ? e.message : 'Failed to send link. Please try again.'
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function reset() {
|
||||
sent.value = false
|
||||
email.value = ''
|
||||
error.value = ''
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.login-form {
|
||||
max-width: 400px;
|
||||
margin: 2rem auto;
|
||||
}
|
||||
|
||||
.sent-state {
|
||||
margin-top: 4rem;
|
||||
}
|
||||
|
||||
.sent-icon {
|
||||
font-size: 4rem;
|
||||
color: var(--ion-color-primary);
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.error-text {
|
||||
color: var(--ion-color-danger);
|
||||
font-size: 0.875rem;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user