Skip to content

Commit 55b5a76

Browse files
authored
re-instate the confirm and deny email route (#1245)
1 parent 6eb8df1 commit 55b5a76

File tree

2 files changed

+129
-0
lines changed

2 files changed

+129
-0
lines changed

frontend/src/router.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import LoginView from '@/views/LoginView.vue';
77
import PostLoginView from '@/views/PostLoginView.vue';
88
import { useUserStore } from '@/stores/user-store';
99
import LogoutView from '@/views/LogoutView.vue';
10+
import BookingConfirmationView from '@/views/BookingConfirmationView.vue';
1011

1112
// lazy loaded components
1213
const AvailabilityView = defineAsyncComponent(() => import('@/views/AvailabilityView/index.vue'));
@@ -82,6 +83,15 @@ const routes: RouteRecordRaw[] = [
8283
maskForMetrics: true,
8384
},
8485
},
86+
{
87+
path: '/user/:username/:signature/confirm/:slot/:token/:confirmed',
88+
name: 'confirmation',
89+
component: BookingConfirmationView,
90+
meta: {
91+
isPublic: true,
92+
maskForMetrics: true,
93+
},
94+
},
8595
{
8696
path: '/availability',
8797
name: 'availability',
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
<script setup lang="ts">
2+
import { ref, inject, onMounted } from 'vue';
3+
import { useI18n } from 'vue-i18n';
4+
import { useRoute } from 'vue-router';
5+
import { callKey } from '@/keys';
6+
import { AvailabilitySlotResponse } from '@/models';
7+
import ArtInvalidLink from '@/elements/arts/ArtInvalidLink.vue';
8+
import ArtSuccessfulBooking from '@/elements/arts/ArtSuccessfulBooking.vue';
9+
import LoadingSpinner from '@/elements/LoadingSpinner.vue';
10+
import { usePosthog, posthog } from '@/composables/posthog';
11+
import { MetricEvents } from '@/definitions';
12+
13+
const { t } = useI18n();
14+
const route = useRoute();
15+
const call = inject(callKey);
16+
17+
// retrieve all required data from url
18+
const [signedUrl] = window.location.href.split('/confirm/');
19+
const slotId = Number(route.params.slot);
20+
const slotToken = route.params.token;
21+
const confirmed = parseInt(route.params.confirmed as string) === 1;
22+
23+
const isError = ref<boolean|null>(null);
24+
const attendeeEmail = ref<string|null>(null);
25+
26+
// initially load data when component gets remounted
27+
onMounted(async () => {
28+
// build data object for put request
29+
const obj = {
30+
slot_id: slotId,
31+
slot_token: slotToken,
32+
owner_url: signedUrl,
33+
confirmed,
34+
};
35+
const { error, data }: AvailabilitySlotResponse = await call('schedule/public/availability/booking').put(obj).json();
36+
if (error.value) {
37+
isError.value = true;
38+
39+
return;
40+
}
41+
42+
isError.value = false;
43+
attendeeEmail.value = data.value?.attendee?.email;
44+
45+
if (usePosthog) {
46+
const event = confirmed ? MetricEvents.ConfirmBooking : MetricEvents.DenyBooking;
47+
posthog.capture(event);
48+
}
49+
});
50+
</script>
51+
52+
<template>
53+
<div class="booking-confirmation-view-container">
54+
<div v-if="isError === null">
55+
<loading-spinner />
56+
</div>
57+
<div v-else-if="isError === true" class="content">
58+
<art-invalid-link class="art" />
59+
<h1>{{ t('info.bookingLinkIsInvalid') }}</h1>
60+
<p>{{ t('text.invalidOrAlreadyBooked') }}</p>
61+
</div>
62+
<div v-else class="content">
63+
<art-successful-booking class="art" />
64+
<template v-if="confirmed">
65+
<h1>{{ t('info.bookingSuccessfullyConfirmed') }}</h1>
66+
<p>
67+
{{ t('info.eventWasCreated') }}<br>
68+
{{ t('text.invitationSentToAddress', { 'address': attendeeEmail }) }}
69+
</p>
70+
</template>
71+
<template v-else>
72+
<h1>{{ t('info.bookingSuccessfullyDenied') }}</h1>
73+
<p>
74+
{{ t('text.denialSentToAddress', { 'address': attendeeEmail }) }}<br>
75+
{{ t('info.slotIsAvailableAgain') }}
76+
</p>
77+
</template>
78+
</div>
79+
</div>
80+
</template>
81+
82+
<style scoped>
83+
.booking-confirmation-view-container {
84+
display: flex;
85+
flex-direction: column;
86+
gap: 3rem;
87+
align-items: center;
88+
justify-content: center;
89+
height: 100%;
90+
padding: 1rem;
91+
92+
.content {
93+
display: flex;
94+
flex-direction: column;
95+
gap: 2rem;
96+
align-items: center;
97+
justify-content: center;
98+
padding-inline: 1rem;
99+
100+
h1 {
101+
font-size: 1.25rem;
102+
line-height: 1.75rem;
103+
font-weight: 600;
104+
color: var(--colour-ti-highlight);
105+
}
106+
107+
p {
108+
text-align: center;
109+
color: var(--colour-ti-secondary);
110+
}
111+
112+
.art {
113+
margin-block: 1.5rem;
114+
height: auto;
115+
max-width: 24rem;
116+
}
117+
}
118+
}
119+
</style>

0 commit comments

Comments
 (0)