diff --git a/backend/src/appointment/database/schemas.py b/backend/src/appointment/database/schemas.py index 8c51ce3bf..b006e82e6 100644 --- a/backend/src/appointment/database/schemas.py +++ b/backend/src/appointment/database/schemas.py @@ -86,6 +86,7 @@ class Slot(SlotBase): class SlotOut(BaseModel): id: int | None = None + slug: str | None = None attendee_id: int | None = None booking_status: BookingStatus | None = BookingStatus.none duration: int | None = None diff --git a/backend/src/appointment/routes/schedule.py b/backend/src/appointment/routes/schedule.py index 65e3877fb..e68c89233 100644 --- a/backend/src/appointment/routes/schedule.py +++ b/backend/src/appointment/routes/schedule.py @@ -443,6 +443,7 @@ def request_schedule_availability_slot( # Mini version of slot, so we can grab the newly created slot id for tests return schemas.SlotOut( id=slot.id, + slug=appointment.slug, start=slot.start, duration=slot.duration, attendee_id=slot.attendee_id, diff --git a/frontend/src/App.vue b/frontend/src/App.vue index e801bc182..62c7200ad 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -371,6 +371,14 @@ main { min-height: 0; margin-inline: 1rem; } + + &.public-route { + display: flex; + flex-direction: column; + flex-grow: 1; + min-height: 0; + padding-block-end: 1rem; + } } @media (--md) { @@ -384,4 +392,4 @@ main { margin-inline: 3.5rem; } } - \ No newline at end of file + diff --git a/frontend/src/assets/styles/colours.css b/frontend/src/assets/styles/colours.css index d74a1c5ac..e0b871c6b 100644 --- a/frontend/src/assets/styles/colours.css +++ b/frontend/src/assets/styles/colours.css @@ -17,6 +17,7 @@ html { --colour-neutral-base: #FEFFFF; --colour-neutral-lower: #F7F7F7; --colour-neutral-lower-dark: #18181B; /* Forced dark mode colour */ + --colour-neutral-lower-light: #F7F7F7; /* Forced light mode colour */ --colour-neutral-raised: var(--colour-neutral-base); --colour-neutral-subtle: var(--colour-neutral-lower); --colour-neutral-border: #E4E4E7; @@ -76,6 +77,7 @@ html { --colour-ti-black: #000000; --colour-ti-base: #1A202C; --colour-ti-base-dark: #EEEEF0; /* Forced dark mode colour */ + --colour-ti-base-light: #1A202C; --colour-ti-secondary: #4C4D58; --colour-ti-muted: #737584; --colour-ti-highlight: #1373D9; diff --git a/frontend/src/assets/svg/appointment_calendar_logo.svg b/frontend/src/assets/svg/appointment_calendar_logo.svg new file mode 100644 index 000000000..6f1be141b --- /dev/null +++ b/frontend/src/assets/svg/appointment_calendar_logo.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/frontend/src/locales/de.json b/frontend/src/locales/de.json index aed61fff4..d9c59b9a6 100644 --- a/frontend/src/locales/de.json +++ b/frontend/src/locales/de.json @@ -1,9 +1,9 @@ { "app": { "summary": "Lass Freunde und Kollegen Zeiten in deinem Kalender wählen. Terminabsprachen so simpel wie möglich.", - "tagline": "Weniger planen, mehr schaffen", - "title": "Thunderbird Appointment" - }, + "tagline": "Weniger planen. Mehr schaffen.", + "title": "Thunderbird Appointment", + "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." }, "calDAVForm": { "help": { "location": "URL oder Hostname, den wir für die Verbindung deiner Kalender verwenden werden.", @@ -296,7 +296,7 @@ "download": "Download", "downloadICS": "Download ICS", "downloadInvitation": "Termineinladung herunterladen", - "downloadTheIcsFile": "ICS-Datei herunterladen", + "downloadTheIcsFile": "Zu Kalender hinzufügen", "downloadMyData": "Alle deine Daten von Appointment herunterladen", "earliestBooking": "Früheste Buchung", "edit": "Bearbeiten", @@ -439,6 +439,7 @@ "startTime": "Startzeit", "startUsingTba": "Starte mit TBA", "status": "Status", + "subscribe": "Abonnieren", "success": "Erfolg", "sync": "Synchronisieren", "syncCalendars": "Kalender synchronisieren", @@ -618,6 +619,9 @@ "noCalendars": "Keine Kalenderkonten verbunden. Füge ein Konto hinzu, um zu beginnen." } }, + "timeHasBeenConfirmed": "Der Termin wurde bestätigt. Thunderbird Appointment hat eine Kalendereinladung an {email} gesendet.", + "hostHasBeenNotified": "Der Gastgeber wurde benachrichtigt. Thunderbird Appointment wird dir eine E-Mail senden, sobald er antwortet.", + "virtualMeetingWith": "Online-Meeting mit {name}", "timesAreDisplayedInLocalTimezone": "Die Zeiten werden in deiner lokalen Zeitzone {timezone} angezeigt.", "titleIsReadyForBookings": "{title} ist für Buchungen bereit", "updateLinkNotice": "Ein Ändern des Benutzernamens oder des Teillinks aktualisiert deinen Link. Alle alten Links werden dann nicht mehr funktionieren.", diff --git a/frontend/src/locales/en.json b/frontend/src/locales/en.json index 444d86460..1a88205cf 100644 --- a/frontend/src/locales/en.json +++ b/frontend/src/locales/en.json @@ -1,8 +1,9 @@ { "app": { "summary": "Invite others to grab times on your calendar. Choose a date. Make appointments as easy as it gets.", - "tagline": "Plan less, do more", - "title": "Thunderbird Appointment" + "tagline": "Plan less. Do more.", + "title": "Thunderbird Appointment", + "description": "Thunderbird Appointment makes it easy to find a time to meet. So you can ditch the admin and streamline your day." }, "calDAVForm": { "help": { @@ -299,7 +300,7 @@ "download": "Download", "downloadICS": "Download ICS", "downloadInvitation": "Download invitation", - "downloadTheIcsFile": "Add to your calendar", + "downloadTheIcsFile": "Add to calendar", "downloadMyData": "Download all your data from Appointment", "earlier": "Earlier", "earliestBooking": "Earliest Booking", @@ -442,6 +443,7 @@ "startTime": "Start time", "startUsingTba": "Try Appointment", "status": "Status", + "subscribe": "Subscribe", "success": "Success", "sync": "Sync", "syncCalendars": "Sync Calendars", @@ -621,6 +623,9 @@ "noCalendars": "No calendar accounts connected. Add an account to get started." } }, + "timeHasBeenConfirmed": "Your time has been confirmed. Thunderbird Appointment has sent a calendar invite to {email}.", + "hostHasBeenNotified": "Your host has been notified. Thunderbird Appointment will email you once they respond.", + "virtualMeetingWith": "Virtual meeting with {name}", "timesAreDisplayedInLocalTimezone": "Times are displayed in your local timezone {timezone}.", "titleIsReadyForBookings": "{title} is ready for bookings", "updateLinkNotice": "Changing your username or slug will change your link. Your old link will no longer work.", diff --git a/frontend/src/models.ts b/frontend/src/models.ts index dcfe26c5c..2c3bfb6e8 100644 --- a/frontend/src/models.ts +++ b/frontend/src/models.ts @@ -409,7 +409,7 @@ export type RemoteEventListResponse = UseFetchReturn; export type ScheduleResponse = UseFetchReturn; export type ScheduleListResponse = UseFetchReturn; export type SignatureResponse = UseFetchReturn; -export type SlotResponse = UseFetchReturn; +export type SlotResponse = UseFetchReturn; export type StringResponse = UseFetchReturn; export type StringListResponse = UseFetchReturn; export type SubscriberResponse = UseFetchReturn; diff --git a/frontend/src/utils.ts b/frontend/src/utils.ts index 373153660..353842781 100644 --- a/frontend/src/utils.ts +++ b/frontend/src/utils.ts @@ -149,7 +149,7 @@ export const timeFormat = (): string => { } const format = Number(user.settings?.timeFormat ?? detected); - return format === 24 ? 'HH:mm' : 'hh:mm A'; + return format === 24 ? 'HH:mm' : 'hh:mma'; }; // Check if we already have a local user preferred language diff --git a/frontend/src/views/BookerView/components/BookingViewSuccess.vue b/frontend/src/views/BookerView/components/BookingViewSuccess.vue index 0a4b2757a..b8165a9c7 100644 --- a/frontend/src/views/BookerView/components/BookingViewSuccess.vue +++ b/frontend/src/views/BookerView/components/BookingViewSuccess.vue @@ -5,55 +5,199 @@ import { useI18n } from 'vue-i18n'; import { useRouter } from 'vue-router'; import { useUserStore } from '@/stores/user-store'; -import ArtSuccessfulBooking from '@/elements/arts/ArtSuccessfulBooking.vue'; -import PrimaryButton from '@/elements/PrimaryButton.vue'; -import { dayjsKey } from '@/keys'; -import { Appointment, Slot } from '@/models'; +import { LinkButton, PrimaryButton } from '@thunderbirdops/services-ui'; +import { apiUrlKey, dayjsKey } from '@/keys'; +import { Appointment, Attendee, Slot } from '@/models'; +import { PhArrowRight, PhDownloadSimple, PhConfetti } from '@phosphor-icons/vue'; const { t } = useI18n(); const router = useRouter(); const dj = inject(dayjsKey); +const apiUrl = inject(apiUrlKey); const user = useUserStore(); // component properties interface Props { selectedEvent: Appointment & Slot, - attendeeEmail: string, + attendee: Attendee, requested: boolean, // True if we are requesting a booking, false if already confirmed } -defineProps(); +const props = defineProps(); + +const heading = props.requested + ? t('info.bookingSuccessfullyRequested') + : t('info.bookingSuccessfullyConfirmed'); + +const description = props.requested + ? t('text.hostHasBeenNotified') + : t('text.timeHasBeenConfirmed', {'email': props.attendee.email}); + +const date = dj(props.selectedEvent.start).format('ddd') + ', ' + + dj(props.selectedEvent.start).format('MMM DD') + ' from ' + + dj(props.selectedEvent.start).format(timeFormat()) + ' – ' + + dj(props.selectedEvent.start).add(props.selectedEvent.duration, 'minutes').format(timeFormat()) + + ' (' + dj.tz.guess() + ')'; + +const downloadUrl = `${apiUrl}/apmt/serve/ics/${props.selectedEvent.slug}/${props.selectedEvent.id}`; + + diff --git a/frontend/src/views/BookerView/components/SlotSelectionAside.vue b/frontend/src/views/BookerView/components/SlotSelectionAside.vue index 56f5c3a2c..777510c37 100644 --- a/frontend/src/views/BookerView/components/SlotSelectionAside.vue +++ b/frontend/src/views/BookerView/components/SlotSelectionAside.vue @@ -92,6 +92,10 @@ const bookEvent = async () => { bookingRequestError.value = ''; + // Enrich the selected event with data we need to request ICS download after booking request + selectedEvent.value.id = data.value.id; + selectedEvent.value.slug = data.value.slug; + // replace calendar view if every thing worked fine attendee.value = attendeeData; // update view to prevent reselection diff --git a/frontend/src/views/BookerView/index.vue b/frontend/src/views/BookerView/index.vue index 79a4fbb67..d6ed1e965 100644 --- a/frontend/src/views/BookerView/index.vue +++ b/frontend/src/views/BookerView/index.vue @@ -150,8 +150,8 @@ export default { class="booking-success-container" > @@ -171,16 +171,12 @@ export default { display: flex; justify-content: center; align-items: center; - height: 100vh; - user-select: none; } .booking-invalid-container { display: flex; justify-content: center; align-items: center; - height: 100vh; - user-select: none; flex-direction: column; gap: 2rem; padding: 0 1rem; @@ -188,15 +184,15 @@ export default { .booking-success-container { display: flex; - height: 100vh; - user-select: none; - flex-direction: column-reverse; + flex-direction: column; align-items: center; - justify-content: space-evenly; + justify-content: center; padding: 0 1rem; + gap: 2rem; } .booking-slot-selection-container { + width: 100%; margin: 0 auto; user-select: none; padding: 0 1rem; @@ -207,6 +203,7 @@ export default { @media (--md) { .booking-success-container { flex-direction: row; + align-items: start; } }