Skip to content

Commit 2c0a554

Browse files
authored
Update booking confirmation UI (#1429)
* ➕ Reset existing page styles * ➕ Add new page content for successful booking * 🔨 Clean up and temp style fixes * 🔨 Typography, color and sizing fixes * 🔨 Use CSS instead of JS for capitalizing text * 🔨 Fix icons * 🔨 Fix CTA button styles * 🔨 Fix text color on dark mode * 🔨 Fix responsiveness * 🔨 Fix ICS download
1 parent 6d4b814 commit 2c0a554

File tree

12 files changed

+225
-49
lines changed

12 files changed

+225
-49
lines changed

backend/src/appointment/database/schemas.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ class Slot(SlotBase):
8686

8787
class SlotOut(BaseModel):
8888
id: int | None = None
89+
slug: str | None = None
8990
attendee_id: int | None = None
9091
booking_status: BookingStatus | None = BookingStatus.none
9192
duration: int | None = None

backend/src/appointment/routes/schedule.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -443,6 +443,7 @@ def request_schedule_availability_slot(
443443
# Mini version of slot, so we can grab the newly created slot id for tests
444444
return schemas.SlotOut(
445445
id=slot.id,
446+
slug=appointment.slug,
446447
start=slot.start,
447448
duration=slot.duration,
448449
attendee_id=slot.attendee_id,

frontend/src/App.vue

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -371,6 +371,14 @@ main {
371371
min-height: 0;
372372
margin-inline: 1rem;
373373
}
374+
375+
&.public-route {
376+
display: flex;
377+
flex-direction: column;
378+
flex-grow: 1;
379+
min-height: 0;
380+
padding-block-end: 1rem;
381+
}
374382
}
375383
376384
@media (--md) {
@@ -384,4 +392,4 @@ main {
384392
margin-inline: 3.5rem;
385393
}
386394
}
387-
</style>
395+
</style>

frontend/src/assets/styles/colours.css

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ html {
1717
--colour-neutral-base: #FEFFFF;
1818
--colour-neutral-lower: #F7F7F7;
1919
--colour-neutral-lower-dark: #18181B; /* Forced dark mode colour */
20+
--colour-neutral-lower-light: #F7F7F7; /* Forced light mode colour */
2021
--colour-neutral-raised: var(--colour-neutral-base);
2122
--colour-neutral-subtle: var(--colour-neutral-lower);
2223
--colour-neutral-border: #E4E4E7;
@@ -76,6 +77,7 @@ html {
7677
--colour-ti-black: #000000;
7778
--colour-ti-base: #1A202C;
7879
--colour-ti-base-dark: #EEEEF0; /* Forced dark mode colour */
80+
--colour-ti-base-light: #1A202C;
7981
--colour-ti-secondary: #4C4D58;
8082
--colour-ti-muted: #737584;
8183
--colour-ti-highlight: #1373D9;
Lines changed: 10 additions & 0 deletions
Loading

frontend/src/locales/de.json

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
{
22
"app": {
33
"summary": "Lass Freunde und Kollegen Zeiten in deinem Kalender wählen. Terminabsprachen so simpel wie möglich.",
4-
"tagline": "Weniger planen, mehr schaffen",
5-
"title": "Thunderbird Appointment"
6-
},
4+
"tagline": "Weniger planen. Mehr schaffen.",
5+
"title": "Thunderbird Appointment",
6+
"description": "Mit Thunderbird Appointment findest Du ganz einfach einen Termin für ein Treffen. So kannst Du Verwaltungsaufwand hinter Dir lassen und Deinen Tag optimieren." },
77
"calDAVForm": {
88
"help": {
99
"location": "URL oder Hostname, den wir für die Verbindung deiner Kalender verwenden werden.",
@@ -296,7 +296,7 @@
296296
"download": "Download",
297297
"downloadICS": "Download ICS",
298298
"downloadInvitation": "Termineinladung herunterladen",
299-
"downloadTheIcsFile": "ICS-Datei herunterladen",
299+
"downloadTheIcsFile": "Zu Kalender hinzufügen",
300300
"downloadMyData": "Alle deine Daten von Appointment herunterladen",
301301
"earliestBooking": "Früheste Buchung",
302302
"edit": "Bearbeiten",
@@ -439,6 +439,7 @@
439439
"startTime": "Startzeit",
440440
"startUsingTba": "Starte mit TBA",
441441
"status": "Status",
442+
"subscribe": "Abonnieren",
442443
"success": "Erfolg",
443444
"sync": "Synchronisieren",
444445
"syncCalendars": "Kalender synchronisieren",
@@ -618,6 +619,9 @@
618619
"noCalendars": "Keine Kalenderkonten verbunden. Füge ein Konto hinzu, um zu beginnen."
619620
}
620621
},
622+
"timeHasBeenConfirmed": "Der Termin wurde bestätigt. Thunderbird Appointment hat eine Kalendereinladung an {email} gesendet.",
623+
"hostHasBeenNotified": "Der Gastgeber wurde benachrichtigt. Thunderbird Appointment wird dir eine E-Mail senden, sobald er antwortet.",
624+
"virtualMeetingWith": "Online-Meeting mit {name}",
621625
"timesAreDisplayedInLocalTimezone": "Die Zeiten werden in deiner lokalen Zeitzone {timezone} angezeigt.",
622626
"titleIsReadyForBookings": "{title} ist für Buchungen bereit",
623627
"updateLinkNotice": "Ein Ändern des Benutzernamens oder des Teillinks aktualisiert deinen Link. Alle alten Links werden dann nicht mehr funktionieren.",

frontend/src/locales/en.json

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
{
22
"app": {
33
"summary": "Invite others to grab times on your calendar. Choose a date. Make appointments as easy as it gets.",
4-
"tagline": "Plan less, do more",
5-
"title": "Thunderbird Appointment"
4+
"tagline": "Plan less. Do more.",
5+
"title": "Thunderbird Appointment",
6+
"description": "Thunderbird Appointment makes it easy to find a time to meet. So you can ditch the admin and streamline your day."
67
},
78
"calDAVForm": {
89
"help": {
@@ -299,7 +300,7 @@
299300
"download": "Download",
300301
"downloadICS": "Download ICS",
301302
"downloadInvitation": "Download invitation",
302-
"downloadTheIcsFile": "Add to your calendar",
303+
"downloadTheIcsFile": "Add to calendar",
303304
"downloadMyData": "Download all your data from Appointment",
304305
"earlier": "Earlier",
305306
"earliestBooking": "Earliest Booking",
@@ -442,6 +443,7 @@
442443
"startTime": "Start time",
443444
"startUsingTba": "Try Appointment",
444445
"status": "Status",
446+
"subscribe": "Subscribe",
445447
"success": "Success",
446448
"sync": "Sync",
447449
"syncCalendars": "Sync Calendars",
@@ -621,6 +623,9 @@
621623
"noCalendars": "No calendar accounts connected. Add an account to get started."
622624
}
623625
},
626+
"timeHasBeenConfirmed": "Your time has been confirmed. Thunderbird Appointment has sent a calendar invite to {email}.",
627+
"hostHasBeenNotified": "Your host has been notified. Thunderbird Appointment will email you once they respond.",
628+
"virtualMeetingWith": "Virtual meeting with {name}",
624629
"timesAreDisplayedInLocalTimezone": "Times are displayed in your local timezone {timezone}.",
625630
"titleIsReadyForBookings": "{title} is ready for bookings",
626631
"updateLinkNotice": "Changing your username or slug will change your link. Your old link will no longer work.",

frontend/src/models.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -409,7 +409,7 @@ export type RemoteEventListResponse = UseFetchReturn<RemoteEvent[]>;
409409
export type ScheduleResponse = UseFetchReturn<Schedule|Exception>;
410410
export type ScheduleListResponse = UseFetchReturn<Schedule[]>;
411411
export type SignatureResponse = UseFetchReturn<Signature>;
412-
export type SlotResponse = UseFetchReturn<Slot|Exception>;
412+
export type SlotResponse = UseFetchReturn<Slot & Appointment|Exception>;
413413
export type StringResponse = UseFetchReturn<string|Exception>;
414414
export type StringListResponse = UseFetchReturn<string[]>;
415415
export type SubscriberResponse = UseFetchReturn<Subscriber>;

frontend/src/utils.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -149,7 +149,7 @@ export const timeFormat = (): string => {
149149
}
150150

151151
const format = Number(user.settings?.timeFormat ?? detected);
152-
return format === 24 ? 'HH:mm' : 'hh:mm A';
152+
return format === 24 ? 'HH:mm' : 'hh:mma';
153153
};
154154

155155
// Check if we already have a local user preferred language

frontend/src/views/BookerView/components/BookingViewSuccess.vue

Lines changed: 174 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -5,55 +5,199 @@ import { useI18n } from 'vue-i18n';
55
import { useRouter } from 'vue-router';
66
import { useUserStore } from '@/stores/user-store';
77
8-
import ArtSuccessfulBooking from '@/elements/arts/ArtSuccessfulBooking.vue';
9-
import PrimaryButton from '@/elements/PrimaryButton.vue';
10-
import { dayjsKey } from '@/keys';
11-
import { Appointment, Slot } from '@/models';
8+
import { LinkButton, PrimaryButton } from '@thunderbirdops/services-ui';
9+
import { apiUrlKey, dayjsKey } from '@/keys';
10+
import { Appointment, Attendee, Slot } from '@/models';
11+
import { PhArrowRight, PhDownloadSimple, PhConfetti } from '@phosphor-icons/vue';
1212
1313
const { t } = useI18n();
1414
const router = useRouter();
1515
1616
const dj = inject(dayjsKey);
17+
const apiUrl = inject(apiUrlKey);
1718
const user = useUserStore();
1819
1920
// component properties
2021
interface Props {
2122
selectedEvent: Appointment & Slot,
22-
attendeeEmail: string,
23+
attendee: Attendee,
2324
requested: boolean, // True if we are requesting a booking, false if already confirmed
2425
}
25-
defineProps<Props>();
26+
const props = defineProps<Props>();
27+
28+
const heading = props.requested
29+
? t('info.bookingSuccessfullyRequested')
30+
: t('info.bookingSuccessfullyConfirmed');
31+
32+
const description = props.requested
33+
? t('text.hostHasBeenNotified')
34+
: t('text.timeHasBeenConfirmed', {'email': props.attendee.email});
35+
36+
const date = dj(props.selectedEvent.start).format('ddd') + ', '
37+
+ dj(props.selectedEvent.start).format('MMM DD') + ' from '
38+
+ dj(props.selectedEvent.start).format(timeFormat()) + ''
39+
+ dj(props.selectedEvent.start).add(props.selectedEvent.duration, 'minutes').format(timeFormat())
40+
+ ' (' + dj.tz.guess() + ')';
41+
42+
const downloadUrl = `${apiUrl}/apmt/serve/ics/${props.selectedEvent.slug}/${props.selectedEvent.id}`;
2643
2744
</script>
2845

2946
<template>
30-
<div class="flex-center min-w-[50%] flex-col gap-12">
31-
<div class="text-2xl font-semibold text-teal-500">
32-
<span v-if="requested">{{ t('info.bookingSuccessfullyRequested') }}</span>
33-
<span v-else>{{ t('info.bookingSuccessfullyConfirmed') }}</span>
47+
<div class="booking-details">
48+
<div class="heading">
49+
<ph-confetti />
50+
{{ heading }}
3451
</div>
35-
<div class="flex w-full max-w-sm flex-col gap-1 rounded-lg shadow-lg dark:bg-gray-800">
36-
<div class="flex h-14 items-center justify-around rounded-t-md bg-teal-500">
37-
<div v-for="i in 2" :key="i" class="size-4 rounded-full bg-white dark:bg-gray-600"></div>
52+
<p>{{ description }}</p>
53+
<div class="info">
54+
<div class="logo">
55+
<img src="@/assets/svg/appointment_calendar_logo.svg" alt="Appointment Calendar Logo" />
3856
</div>
39-
<div class="m-2 text-center text-2xl font-bold text-gray-500 dark:text-gray-300">
40-
{{ selectedEvent.title }}
41-
</div>
42-
<div class="m-2 flex flex-col gap-0.5 rounded-md bg-gray-100 py-2 text-center text-gray-500 dark:bg-gray-700 dark:text-gray-300">
43-
<div class="text-sm font-semibold text-teal-500">{{ dj(selectedEvent.start).format('dddd') }}</div>
44-
<div class="text-lg">{{ dj(selectedEvent.start).format('LL') }}</div>
45-
<div class="flex-center gap-2 text-sm uppercase">
46-
<span>{{ dj(selectedEvent.start).format(timeFormat()) }}</span>
47-
<span>{{ dj.tz.guess() }}</span>
48-
</div>
57+
<div>
58+
{{ date }}
59+
<br />
60+
{{ t('text.virtualMeetingWith', {name: attendee.name}) }}
4961
</div>
5062
</div>
51-
<primary-button
52-
v-if="!user.authenticated"
53-
class="btn-start mt-12 p-7"
54-
:label="t('label.startUsingTba')"
55-
@click="router.push({ name: 'home' })"
56-
/>
63+
<div class="actions">
64+
<link-button :href="downloadUrl">
65+
<template #iconLeft>
66+
<ph-download-simple />
67+
</template>
68+
{{ t('label.downloadTheIcsFile') }}
69+
</link-button>
70+
</div>
71+
</div>
72+
<div class="appointment-call-out">
73+
<img src="@/assets/svg/appointment_logo.svg" alt="Appointment Logo" />
74+
<span class="tagline" v-text="t('app.tagline')"></span>
75+
<span class="description" v-text="t('app.description')"></span>
76+
<primary-button @click="router.push({ name: 'home' })">
77+
{{ user.authenticated ? t('label.dashboard') : t('label.subscribe') }}
78+
<template #iconRight>
79+
<ph-arrow-right weight="bold" />
80+
</template>
81+
</primary-button>
5782
</div>
58-
<art-successful-booking class="m-6 h-auto w-full max-w-md sm:w-auto sm:max-w-md"/>
5983
</template>
84+
85+
<style scoped>
86+
@import '@/assets/styles/custom-media.pcss';
87+
88+
.booking-details {
89+
border-radius: 1rem;
90+
padding: 2rem 1.5rem;
91+
max-width: 48rem;
92+
93+
display: flex;
94+
flex-direction: column;
95+
gap: 1.5rem;
96+
97+
background-color: var(--colour-neutral-base);
98+
font-family: Inter, sans-serif;
99+
100+
.heading {
101+
display: flex;
102+
align-items: center;
103+
gap: 0.5rem;
104+
105+
color: var(--colour-ti-highlight);
106+
font-size: 1.5rem;
107+
text-transform: capitalize;
108+
109+
svg {
110+
fill: var(--colour-ti-highlight);
111+
}
112+
}
113+
114+
p {
115+
color: var(--colour-ti-base);
116+
}
117+
118+
.info {
119+
display: flex;
120+
flex-direction: column;
121+
gap: 1.5rem;
122+
font-size: 1.25rem;
123+
color: var(--colour-ti-black);
124+
125+
.logo {
126+
padding: 0.5rem;
127+
border-radius: 1rem;
128+
background-image: linear-gradient(#ffffff, #bee1fe);
129+
flex-shrink: 0;
130+
align-self: center;
131+
}
132+
}
133+
134+
.actions {
135+
display: flex;
136+
137+
a {
138+
padding: 0;
139+
color: var(--colour-ti-highlight);
140+
font-size: .75rem;
141+
}
142+
143+
:deep(.base.link.filled) .icon,
144+
:deep(.base.link.filled) .icon svg {
145+
width: 16px !important;
146+
height: 16px !important;
147+
}
148+
}
149+
}
150+
@media (--sm) {
151+
.booking-details .info {
152+
flex-direction: row;
153+
}
154+
}
155+
156+
.appointment-call-out {
157+
display: flex;
158+
flex-direction: column;
159+
gap: 1.5rem;
160+
justify-content: center;
161+
align-items: center;
162+
163+
border-radius: 1rem;
164+
padding: 1.5rem 1.5rem 3.5rem;
165+
max-width: 23rem;
166+
167+
background-image: radial-gradient(circle at bottom right, #336d71, #1b222e 85%);
168+
color: var(--colour-neutral-lower-light);
169+
font-family: Inter, sans-serif;
170+
text-align: center;
171+
172+
.tagline {
173+
font-size: 2rem;
174+
font-weight: 300;
175+
font-family: Metropolis, sans-serif;
176+
}
177+
178+
.description {
179+
font-size: 0.875rem;
180+
color: var(--colour-neutral-lower-light);
181+
}
182+
183+
:deep(.base.primary.filled) {
184+
position: relative;
185+
z-index: 1;
186+
background-image: linear-gradient(161deg, #a0e1ff -26%, #2b8cdc 45%);
187+
color: var(--colour-ti-base-light);
188+
text-transform: uppercase;
189+
font-weight: 600;
190+
font-size: 0.8125rem;
191+
192+
&::before {
193+
content: '';
194+
position: absolute;
195+
z-index: -1;
196+
width: calc(100% - 2px);
197+
height: calc(100% - 2px);
198+
background-image: linear-gradient(353deg, #1373d9 -36%, #58c9ff);
199+
border-radius: .5rem;
200+
}
201+
}
202+
}
203+
</style>

0 commit comments

Comments
 (0)