Skip to content

Commit

Permalink
* Move time formatting to scheduleStore
Browse files Browse the repository at this point in the history
* Lots of small formatting tweaks
* Fix bug where a user with no calendars connected can get to the connect calendar screen
* Add force param to calendarStore.fetch
* Add form elements and hook up the enter key
  • Loading branch information
MelissaAutumn committed Jun 25, 2024
1 parent e665bd7 commit 6fdf119
Show file tree
Hide file tree
Showing 9 changed files with 131 additions and 53 deletions.
6 changes: 5 additions & 1 deletion frontend/src/components/FTUE/ConnectCalendars.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,15 @@
<notice-bar>
Connect your calendars to manage your availability
</notice-bar>
<form autocomplete="off" autofocus @submit.prevent @keyup.enter="onSubmit">
<sync-card class="sync-card" v-model="calendars" title="Calendars">
<template v-slot:icon>
<span class="icon-calendar">
<img src="@/assets/svg/icons/calendar.svg" alt="calendar icon" title="calendar icon"/>
</span>
</template>
</sync-card>
</form>
</div>
<div class="absolute bottom-[5.75rem] flex w-full justify-end gap-4">
<secondary-button
Expand Down Expand Up @@ -63,12 +65,14 @@ const selected = computed(() => calendars.value.filter((item) => item.checked).l
const continueTitle = computed(() => (selected.value ? 'Continue' : 'Please enable one calendar to continue'));
onMounted(async () => {
await calendarStore.fetch(call);
isLoading.value = true;
await calendarStore.fetch(call, true);
calendars.value = calendarStore.calendars.map((calendar) => ({
key: calendar.id,
label: calendar.title,
checked: calendar.connected,
}));
isLoading.value = false;
});
const onSubmit = async () => {
Expand Down
4 changes: 1 addition & 3 deletions frontend/src/components/FTUE/Finish.vue
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,10 @@
<script setup>
import { useI18n } from 'vue-i18n';
import {
onMounted, inject, ref, computed,
onMounted, inject, ref,
} from 'vue';
import { useFTUEStore } from '@/stores/ftue-store';
import { useCalendarStore } from '@/stores/calendar-store';
import { useScheduleStore } from '@/stores/schedule-store';
import { storeToRefs } from 'pinia';
import PrimaryButton from '@/tbpro/elements/PrimaryButton.vue';
import { useUserStore } from '@/stores/user-store';
Expand Down
35 changes: 30 additions & 5 deletions frontend/src/components/FTUE/GooglePermissions.vue
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
<template>
<div class="flex w-full flex-col gap-4">
<div class="content">
<div class="card">
<notice-bar v-if="errorMessage">
{{ errorMessage }}
</notice-bar>
<p class="mb-2 text-lg">{{ t('text.googlePermissionDisclaimer') }}</p>
<ul class="text-md mx-8 list-disc">
<p class="">{{ t('text.googlePermissionDisclaimer') }}</p>
<ul class="">
<li>
<strong>
{{ t('text.googlePermissionEventsName') }}
Expand All @@ -30,6 +31,7 @@
</a>
</i18n-t>
</div>
</div>
<div class="absolute bottom-[5.75rem] flex w-full justify-end gap-4">
<secondary-button
class="btn-back"
Expand All @@ -55,7 +57,7 @@
<script setup>
import { useI18n } from 'vue-i18n';
import {
defineEmits, onMounted, inject, ref,
onMounted, inject, ref,
} from 'vue';
import SecondaryButton from '@/tbpro/elements/SecondaryButton.vue';
import { useFTUEStore } from '@/stores/ftue-store';
Expand All @@ -80,11 +82,15 @@ const {
const { previousStep, nextStep } = ftueStore;
const calendarStore = useCalendarStore();
const { calendars } = storeToRefs(calendarStore);
const initFlowKey = 'tba/startedCalConnect';
onMounted(async () => {
await calendarStore.fetch(call);
const hasFlowKey = localStorage?.getItem(initFlowKey);
// Error occurred during flow
if (route.query.error) {
if (route.query.error || (hasFlowKey && calendars.value.length === 0)) {
localStorage?.removeItem(initFlowKey);
errorMessage.value = route.query.error;
await router.replace(route.path);
Expand All @@ -109,6 +115,25 @@ const onSubmit = async () => {
</script>
<style scoped>
.content {
display: flex;
height: 24rem;
margin: auto;
}
.card {
display: flex;
flex-direction: column;
gap: 1rem;
width: 70%;
padding: 1rem;
border-radius: 0.5625rem;
background-color: color-mix(in srgb, var(--neutral) 65%, transparent);
border: 0.0625rem solid color-mix(in srgb, var(--neutral) 65%, transparent);
font-size: 0.8125rem;
margin: auto;
}
.google-calendar-logo {
display: inline-block;
background-image: url('@/assets/svg/google-calendar-logo.svg');
Expand Down
6 changes: 3 additions & 3 deletions frontend/src/components/FTUE/SetupProfile.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import TextInput from '@/tbpro/elements/TextInput.vue';
import SelectInput from '@/tbpro/elements/SelectInput.vue';
import {
inject, onMounted, ref,
inject, ref,
} from 'vue';
import PrimaryButton from '@/tbpro/elements/PrimaryButton.vue';
import { storeToRefs } from 'pinia';
Expand Down Expand Up @@ -59,7 +59,7 @@ const onSubmit = async () => {
<template>
<div class="flex w-full max-w-sm flex-col gap-4">
<form ref="formRef" class="flex flex-col" autocomplete="off" autofocus>
<form ref="formRef" class="flex flex-col" autocomplete="off" autofocus @submit.prevent @keyup.enter="onSubmit">
<text-input name="full-name" v-model="fullName" required>Full Name</text-input>
<text-input name="username" v-model="username" required>Username</text-input>
<select-input name="timezone" :options="timezoneOptions" v-model="timezone" required>Timezone</select-input>
Expand All @@ -70,7 +70,7 @@ const onSubmit = async () => {
class="btn-continue"
title="Continue"
v-if="hasNextStep"
@click="onSubmit()"
@click="onSubmit"
>Continue
</primary-button>
</div>
Expand Down
80 changes: 47 additions & 33 deletions frontend/src/components/FTUE/SetupSchedule.vue
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ const calendarStore = useCalendarStore();
const scheduleStore = useScheduleStore();
const { connectedCalendars } = storeToRefs(calendarStore);
const { schedules } = storeToRefs(scheduleStore);
const { timeToBackendTime, timeToFrontendTime } = scheduleStore;
const calendarOptions = computed(() => connectedCalendars.value.map((calendar) => ({
label: calendar.title,
Expand All @@ -52,14 +53,16 @@ const scheduleDayOptions = isoWeekdays.map((day) => ({
const formRef = ref();
const errorMessage = ref(null);
const scheduleName = ref(`${user.data.name}'s Availability`);
const calendar = ref(0);
const startTime = ref('09:00');
const endTime = ref('17:00');
const bookingDuration = ref(defaultSlotDuration);
const scheduleDays = ref([1, 2, 3, 4, 5]);
const schedule = ref({
name: `${user.data.name}'s Availability`,
calendar: 0,
startTime: '09:00',
endTime: '17:00',
duration: defaultSlotDuration,
days: [1, 2, 3, 4, 5],
});
const duration = computed(() => `${bookingDuration.value} minute`);
const duration = computed(() => `${schedule.value.duration} minute`);
const isLoading = ref(false);
const onSubmit = async () => {
Expand All @@ -70,27 +73,20 @@ const onSubmit = async () => {
return;
}
const startTimeFormatted = dj(`${dj().format('YYYY-MM-DD')}T${startTime.value}:00`)
.tz(user.data.timezone ?? dj.tz.guess(), true)
.utc()
.format('HH:mm');
const endTimeFormatted = dj(`${dj().format('YYYY-MM-DD')}T${endTime.value}:00`)
.tz(user.data.timezone ?? dj.tz.guess(), true)
.utc()
.format('HH:mm');
const scheduleData = {
...schedules?.value[0] ?? {},
active: true,
name: scheduleName.value,
calendar_id: calendar.value,
start_time: startTimeFormatted,
end_time: endTimeFormatted,
slot_duration: bookingDuration.value,
weekdays: scheduleDays.value,
name: schedule.value.name,
calendar_id: schedule.value.calendar,
start_time: timeToBackendTime(schedule.value.startTime),
end_time: timeToBackendTime(schedule.value.endTime),
slot_duration: schedule.value.duration,
weekdays: schedule.value.days,
};
const data = schedules.value.length > 0 ? await scheduleStore.updateSchedule(call, schedules.value[0].id, scheduleData) : await scheduleStore.createSchedule(call, scheduleData);
const data = schedules.value.length > 0
? await scheduleStore.updateSchedule(call, schedules.value[0].id, scheduleData)
: await scheduleStore.createSchedule(call, scheduleData);
console.log(data);
if (data?.error) {
Expand All @@ -103,12 +99,31 @@ const onSubmit = async () => {
};
onMounted(async () => {
isLoading.value = true;
await Promise.all([
calendarStore.fetch(call),
scheduleStore.fetch(call),
]);
calendar.value = connectedCalendars.value[0].id;
console.log(schedules.value);
schedule.value.calendar = connectedCalendars.value[0].id;
if (schedules?.value && schedules.value[0]) {
const dbSchedule = schedules.value[0];
schedule.value = {
...schedule.value,
name: dbSchedule.name,
calendar: dbSchedule.calendar_id,
startTime: timeToFrontendTime(dbSchedule.start_time),
endTime: timeToFrontendTime(dbSchedule.end_time),
duration: dbSchedule.slot_duration,
days: dbSchedule.weekdays,
};
}
isLoading.value = false;
});
</script>
Expand All @@ -121,18 +136,18 @@ onMounted(async () => {
<notice-bar v-else>
You can edit this schedule later
</notice-bar>
<form ref="formRef" autocomplete="off" autofocus>
<form ref="formRef" autocomplete="off" autofocus @submit.prevent @keyup.enter="onSubmit">
<div class="column">
<text-input name="scheduleName" v-model="scheduleName" required>Schedule's Name</text-input>
<text-input name="scheduleName" v-model="schedule.name" required>Schedule's Name</text-input>
<div class="pair">
<text-input type="time" name="startTime" v-model="startTime" required>Start Time</text-input>
<text-input type="time" name="endTime" v-model="endTime" required>End Time</text-input>
<text-input type="time" name="startTime" v-model="schedule.startTime" required>Start Time</text-input>
<text-input type="time" name="endTime" v-model="schedule.endTime" required>End Time</text-input>
</div>
<bubble-select :options="scheduleDayOptions" v-model="scheduleDays" />
<bubble-select :options="scheduleDayOptions" v-model="schedule.days" />
</div>
<div class="column">
<select-input name="calendar" v-model="calendar" :options="calendarOptions" required>Select Calendar</select-input>
<select-input name="duration" v-model="bookingDuration" :options="durationOptions" required>Booking Duration</select-input>
<select-input name="calendar" v-model="schedule.calendar" :options="calendarOptions" required>Select Calendar</select-input>
<select-input name="duration" v-model="schedule.duration" :options="durationOptions" required>Booking Duration</select-input>
<div class="scheduleInfo">{{
t('text.recipientsCanScheduleBetween', {
duration: duration,
Expand All @@ -151,8 +166,7 @@ onMounted(async () => {
v-if="hasPreviousStep"
:disabled="isLoading"
@click="previousStep()"
>Back
</secondary-button>
>Back</secondary-button>
<primary-button
class="btn-continue"
title="Continue"
Expand Down
5 changes: 3 additions & 2 deletions frontend/src/stores/calendar-store.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,10 @@ export const useCalendarStore = defineStore('calendars', () => {
/**
* Get all calendars for current user
* @param {function} call preconfigured API fetch function
* @param {boolean} force force a refetch
*/
const fetch = async (call) => {
if (isLoaded.value) {
const fetch = async (call, force = false) => {
if (isLoaded.value && !force) {
return;
}

Expand Down
35 changes: 33 additions & 2 deletions frontend/src/stores/schedule-store.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import { i18n } from '@/composables/i18n';
import { defineStore } from 'pinia';
import { ref, computed } from 'vue';
import { ref, computed, inject } from 'vue';
import { useUserStore } from '@/stores/user-store';
import { dateFormatStrings } from '@/definitions.js';

// eslint-disable-next-line import/prefer-default-export
export const useScheduleStore = defineStore('schedules', () => {
const dj = inject('dayjs');

// State
const isLoaded = ref(false);

Expand Down Expand Up @@ -121,7 +124,35 @@ export const useScheduleStore = defineStore('schedules', () => {
return data;
};

/**
* Converts a time (startTime or endTime) to a timezone that the backend expects
* @param {string} time
*/
const timeToBackendTime = (time) => {
const dateFormat = dateFormatStrings.qalendarFullDay;

const user = useUserStore();
return dj(`${dj().format(dateFormat)}T${time}:00`)
.tz(user.data.timezone ?? dj.tz.guess(), true)
.utc()
.format('HH:mm');
};

/**
* Converts a time (startTime or endTime) to the user's timezone from utc
* @param {string} time
*/
const timeToFrontendTime = (time) => {
const dateFormat = dateFormatStrings.qalendarFullDay;
const user = useUserStore();

return dj(`${dj().format(dateFormat)}T${time}:00`)
.utc(true)
.tz(user.data.timezone ?? dj.tz.guess())
.format('HH:mm');
};

return {
isLoaded, schedules, inactiveSchedules, activeSchedules, fetch, $reset, createSchedule, updateSchedule,
isLoaded, schedules, inactiveSchedules, activeSchedules, fetch, $reset, createSchedule, updateSchedule, timeToBackendTime, timeToFrontendTime,
};
});
2 changes: 1 addition & 1 deletion frontend/src/tbpro/elements/BaseButton.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<template>
<button :class="{'primary': type === 'primary', 'secondary': type === 'secondary', 'small': size === 'small'}">
<button :class="{'primary': type === 'primary', 'secondary': type === 'secondary', 'small': size === 'small'}" type="button">
<span class="icon">
<slot name="icon"/>
</span>
Expand Down
11 changes: 8 additions & 3 deletions frontend/src/views/FirstTimeUserExperienceView.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<template>
<div class="page-ftue overlay fixed left-0 top-0 z-[55] h-screen w-screen overflow-hidden" role="dialog" tabindex="-1" aria-labelledby="ftue-title" aria-modal="true">
<div class="page-ftue overlay" role="dialog" tabindex="-1" aria-labelledby="ftue-title" aria-modal="true">
<div class="modal">
<div class="relative flex size-full w-full flex-col items-center gap-4">
<word-mark v-if="currentStep === ftueStep.setupProfile || currentStep === ftueStep.finish"/>
Expand Down Expand Up @@ -60,9 +60,14 @@ onMounted(() => {
@import '@/assets/styles/custom-media.pcss';
.overlay {
position: fixed;
display: flex;
width: 100%;
height: 100%;
left: 0;
top: 0;
z-index: 55;
width: 100vw;
height: 100vh;
overflow: hidden;
background-color: #727375;
align-items: center;
justify-content: center;
Expand Down

0 comments on commit 6fdf119

Please sign in to comment.