Skip to content

Commit

Permalink
Schedule creation step 4, Timezone step 1 (#650)
Browse files Browse the repository at this point in the history
* ➕ Add step 4 for schedule creation

* ➕ Add slug input field (wip)

* ➕ Extend TextInput for prefix text

* 🔨 Restore code

* 🔨 Implement review feedback

* 🔨 Update element class

* 🔨 Integrate slug into existing schedule object

* ➕ Add bubble select for weekday selection

* ➕ Add timezone information to step 1
  • Loading branch information
devmount authored Sep 12, 2024
1 parent fe5f760 commit 7180ead
Show file tree
Hide file tree
Showing 9 changed files with 1,651 additions and 857 deletions.
2,179 changes: 1,406 additions & 773 deletions frontend/package-lock.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
"tailwindcss": "^3.4.3",
"ua-parser-js": "^1.0.38",
"vite": "^5.0.13",
"vue": "^3.2.13",
"vue": "^3.5.4",
"vue-i18n": "^9.2.2",
"vue-router": "^4.0.3"
},
Expand Down
151 changes: 118 additions & 33 deletions frontend/src/components/ScheduleCreation.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import {
DEFAULT_SLOT_DURATION, SLOT_DURATION_OPTIONS,
DateFormatStrings, EventLocationType, MeetingLinkProviderType, ScheduleCreationState,
} from '@/definitions';
import { Calendar, Schedule, Slot, ScheduleAppointment, Error } from "@/models";
import { Calendar, Schedule, Slot, ScheduleAppointment, Error, SelectOption } from "@/models";
import {
ref, reactive, computed, inject, watch, onMounted, Ref
} from 'vue';
Expand All @@ -19,6 +19,10 @@ import AlertBox from '@/elements/AlertBox.vue';
import SwitchToggle from '@/elements/SwitchToggle.vue';
import ToolTip from '@/elements/ToolTip.vue';
import SnackishBar from '@/elements/SnackishBar.vue';
import BubbleSelect from '@/tbpro/elements/BubbleSelect.vue';
import TextInput from '@/tbpro/elements/TextInput.vue';
import LinkButton from '@/tbpro/elements/LinkButton.vue';
import RefreshIcon from '@/tbpro/icons/RefreshIcon.vue';
// icons
import { IconChevronDown, IconInfoCircle } from '@tabler/icons-vue';
Expand All @@ -27,6 +31,7 @@ import { IconChevronDown, IconInfoCircle } from '@tabler/icons-vue';
import { useCalendarStore } from '@/stores/calendar-store';
import { useExternalConnectionsStore } from '@/stores/external-connections-store';
import { useScheduleStore } from '@/stores/schedule-store';
import router from '@/router';
// component constants
const user = useUserStore();
Expand Down Expand Up @@ -66,6 +71,7 @@ const state = ref(firstStep);
const activeStep1 = computed(() => state.value === firstStep);
const activeStep2 = computed(() => state.value === ScheduleCreationState.Settings);
const activeStep3 = computed(() => state.value === ScheduleCreationState.Details);
const activeStep4 = computed(() => state.value === ScheduleCreationState.Booking);
const visitedStep1 = ref(false);
// calculate calendar titles
Expand Down Expand Up @@ -94,6 +100,7 @@ const defaultSchedule: Schedule = {
weekdays: [1, 2, 3, 4, 5],
slot_duration: DEFAULT_SLOT_DURATION,
meeting_link_provider: MeetingLinkProviderType.None,
slug: user.mySlug,
booking_confirmation: true,
};
const scheduleInput = ref({ ...defaultSchedule });
Expand Down Expand Up @@ -171,12 +178,20 @@ const getScheduleAppointment = (): ScheduleAppointment => ({
type: 'schedule',
});
const isFormDirty = computed(() => JSON.stringify(scheduleInput.value) !== JSON.stringify(referenceSchedule.value));
const isFormDirty = computed(
() => JSON.stringify(scheduleInput.value) !== JSON.stringify(referenceSchedule.value)
);
// handle notes char limit
const charLimit = 250;
const charCount = computed(() => scheduleInput.value.details.length);
// Weekday options
const scheduleDayOptions: SelectOption[] = isoWeekdays.map((day) => ({
label: day.min[0],
value: day.iso,
}));
// booking options
const earliestOptions = {};
[0, 0.5, 1, 2, 3, 4, 5].forEach((d) => {
Expand Down Expand Up @@ -214,8 +229,8 @@ const closeCreatedModal = () => {
savedConfirmation.show = false;
};
// reset the Schedule creation form
const resetSchedule = (resetData = true) => {
// Revert the Schedule creation form to its initial values
const revertForm = (resetData = true) => {
scheduleCreationError.value = null;
if (resetData) {
scheduleInput.value = { ...referenceSchedule.value };
Expand Down Expand Up @@ -314,7 +329,12 @@ const saveSchedule = async (withConfirmation = true) => {
emit('created');
// Update our reference schedule!
referenceSchedule.value = { ...scheduleInput.value };
resetSchedule(false);
revertForm(false);
};
// Update slug with a random 8 character string
const refreshSlug = () => {
scheduleInput.value.slug = self.crypto.randomUUID().substring(0, 8);
};
// handle schedule activation / deactivation
Expand Down Expand Up @@ -360,6 +380,7 @@ watch(
// track changes and send schedule updates
watch(
() => [
scheduleInput.value.active,
scheduleInput.value.name,
scheduleInput.value.calendar_id,
scheduleInput.value.start_date,
Expand Down Expand Up @@ -415,6 +436,7 @@ watch(
<div v-if="!scheduleInput.name" class="content-center text-red-500">*</div>
</label>
</div>

<!-- step 1 -->
<div
class="mx-4 flex flex-col gap-2 rounded-lg border border-zinc-200 p-4 text-gray-700 dark:border-gray-500 dark:bg-gray-600 dark:text-gray-100"
Expand Down Expand Up @@ -468,25 +490,22 @@ watch(
<div class="mb-1 text-sm font-medium text-gray-500 dark:text-gray-300">
{{ t("label.availableDays") }}
</div>
<div class="grid grid-flow-col grid-cols-2 grid-rows-4 gap-2 rounded-lg p-4">
<label
v-for="w in isoWeekdays"
:key="w.iso"
class="flex cursor-pointer select-none items-center gap-2 text-sm"
>
<input
type="checkbox"
v-model="scheduleInput.weekdays"
:value="w.iso"
:disabled="!scheduleInput.active"
class="size-5 text-teal-500"
/>
<span>{{ w.long }}</span>
</label>
<bubble-select class="bubble-select" :options="scheduleDayOptions" v-model="scheduleInput.weekdays" />
</div>
<div>
<div class="mb-1 text-sm font-medium text-gray-500 dark:text-gray-300">
{{ t("label.timeZone") }}
</div>
<div class="flex justify-between">
<div class="text-gray-600 dark:text-gray-200">{{ user.data.timezone ?? dj.tz.guess() }}</div>
<link-button class="edit-link-btn" @click="router.push({ name: 'settings' })" :tooltip="t('label.editInSettings')">
{{ t('label.edit') }}
</link-button>
</div>
</div>
</div>
</div>

<!-- step 2 -->
<div
class="mx-4 flex flex-col gap-2 rounded-lg border border-zinc-200 p-4 text-gray-700 dark:border-gray-500 dark:bg-gray-600 dark:text-gray-100"
Expand Down Expand Up @@ -594,6 +613,7 @@ watch(
</div>
</div>
</div>

<!-- step 3 -->
<div
@click="state = ScheduleCreationState.Details"
Expand Down Expand Up @@ -673,18 +693,75 @@ watch(
</label>
</div>
</div>
<!-- option to deactivate confirmation -->
<div class="px-4">
<switch-toggle
class="my-1 pr-3 text-sm font-medium text-gray-500 dark:text-gray-300"
:active="schedule.booking_confirmation"
:label="t('label.bookingConfirmation')"
:disabled="!scheduleInput.active"
@changed="toggleBookingConfirmation"
no-legend
/>
<div class="text-xs">
{{ t('text.ownerNeedsToConfirmBooking') }}

<!-- step 4 -->
<div
@click="state = ScheduleCreationState.Booking"
class="btn-step-3 mx-4 flex flex-col gap-2 rounded-lg border border-zinc-200 p-4 text-gray-700 dark:border-gray-500 dark:bg-gray-600 dark:text-gray-100"
:class="{'bg-neutral-50': activeStep4}"
id="schedule-details"
>
<div class="flex cursor-pointer items-center justify-between">
<div class="flex flex-col gap-1">
<h2>
{{ t("label.bookingSettings") }}
</h2>
<h3 class="text-xs">
{{ t('label.scheduleSettings.bookingSettingsSubHeading') }}
</h3>
</div>
<icon-chevron-down
class="size-6 -rotate-90 fill-transparent stroke-gray-800 stroke-1 transition-transform dark:stroke-gray-100"
:class="{ '!rotate-0': activeStep4 }"
/>
</div>
<div v-show="activeStep4" class="flex flex-col gap-2">
<hr/>
<!-- custom quick link -->
<label class="relative flex flex-col gap-1">
<div class="font-medium text-gray-500 dark:text-gray-300">
{{ t("label.quickLink") }}
</div>
<div class="flex gap-2">
<text-input
type="text"
name="slug"
:prefix="`/${user.data.username}/`"
v-model="scheduleInput.slug"
class="w-full rounded-md disabled:cursor-not-allowed"
/>
<refresh-icon class="cursor-pointer mt-2.5 text-teal-600" @click.prevent="refreshSlug" />
</div>
</label>
<!-- option to deactivate confirmation -->
<div class="flex flex-col gap-3">
<switch-toggle
class="my-1 text-sm font-medium text-gray-500 dark:text-gray-300"
:active="schedule.booking_confirmation"
:label="t('label.bookingConfirmation')"
:disabled="!scheduleInput.active"
@changed="toggleBookingConfirmation"
no-legend
/>
<div class="whitespace-pre-line rounded-lg bg-white p-4 text-xs text-gray-500 dark:bg-gray-800">
<div>
{{ t('text.yourQuickLinkIs', { url: user.myLink }) }}<br />
<i18n-t keypath="text.toUpdateYourUsername.text" tag="span">
<template v-slot:link>
<router-link class="underline" :to="{ name: 'settings' }" target="_blank">
{{ t('text.toUpdateYourUsername.link') }}
</router-link>
</template>
</i18n-t>
<span v-if="scheduleInput.booking_confirmation">
{{ t('text.bookingsWillRequireToBeConfirmed') }}
</span>
<span v-else>
{{ t('text.bookingsWillAutomaticallyBeConfirmed') }}
</span>
</div>
</div>
</div>
</div>
</div>
</div>
Expand Down Expand Up @@ -722,7 +799,7 @@ watch(
<secondary-button
:label="t('label.revert')"
class="btn-revert w-1/2"
@click="resetSchedule()"
@click="revertForm()"
:disabled="!scheduleInput.active"
:title="t('label.revert')"
/>
Expand Down Expand Up @@ -791,4 +868,12 @@ input[type=checkbox]:disabled {
.tooltip-icon:hover ~ .tooltip {
display: block;
}

.bubble-select {
gap: .25rem;
}

.edit-link-btn {
min-width: auto;
}
</style>
1 change: 1 addition & 0 deletions frontend/src/definitions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ export enum ScheduleCreationState {
Availability = 1,
Settings = 2,
Details = 3,
Booking = 4,
}

/**
Expand Down
14 changes: 12 additions & 2 deletions frontend/src/locales/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,7 @@
"assign": "Zuweisen",
"attendees": "Teilnehmer",
"availabilityDay": "Verfügbarer Tag",
"availableDays": "Verfügbare Tage",
"availableDays": "Verfügbare Wochentage",
"back": "Zurück",
"bookEvent": "Buchen",
"booked": "Gebucht",
Expand Down Expand Up @@ -207,6 +207,7 @@
"earliestBooking": "Früheste Buchung",
"edit": "Bearbeiten",
"editCalendar": "Kalender bearbeiten",
"editInSettings": "Unter Einstellungen bearbeiten",
"editProfile": "Profil bearbeiten",
"email": "E-Mail",
"end": "Ende",
Expand Down Expand Up @@ -257,6 +258,7 @@
"preferredEmail": "Bevorzugte E-Mail-Adresse",
"primaryTimeZone": "Primäre Zeitzone",
"privacy": "Datenschutz",
"quickLink": "Kurzlink",
"refresh": "Erneuern",
"refreshLink": "Link erneuern",
"remove": "Entfernen",
Expand All @@ -271,6 +273,7 @@
"scheduleDetails": "Zeitplan Details",
"scheduleSettings": {
"availabilitySubHeading": "Lege deine Verfügbarkeit fest",
"bookingSettingsSubHeading": "Kurzlink bearbeiten und Sonstiges",
"meetingDetailsSubHeading": "Videos und/oder Notizen den Events hinzufügen",
"schedulingDetailsSubHeading": "Buchungsintervalle und Dauer festlegen"
},
Expand Down Expand Up @@ -362,6 +365,8 @@
"text": {
"accountDataNotice": "Lade all deine Daten von Thunderbird Appointment herunter.",
"accountDeletionWarning": "Achtung: Die Löschung des Benutzerkontos ist dauerhaft! Du wirst all deine Daten von Thunderbird Appointment verlieren. Deine verbundenen Kalender bleiben selbstverständlich unverändert. Du kannst jederzeit ein neues Benutzerkonto erstellen.",
"bookingsWillAutomaticallyBeConfirmed": "Buchungen werden automatisch bestätigt und zum Kalender hinzugefügt.",
"bookingsWillRequireToBeConfirmed": "Buchungen müssen bestätigt oder abgelehnt werden.",
"calendarDeletionWarning": "Wenn die Verbindung zu diesem Kalender getrennt wird, werden alle Termine und Zeitpläne aus Thunderbird Appointment entfernt. Es werden keine Termine entfernt, die derzeit in diesem Kalender gespeichert sind.",
"chooseDateAndTime": "Wähle Tag und Zeit für ein Treffen.",
"connectZoom": "Du kannst dein Zoom-Konto verbinden, um Besprechungen direkt mit einer Zoom-Einladung zu erstellen.",
Expand Down Expand Up @@ -438,8 +443,13 @@
},
"timesAreDisplayedInLocalTimezone": "Die Zeiten werden in deiner lokalen Zeitzone {timezone} angezeigt.",
"titleIsReadyForBookings": "{title} ist für Buchungen bereit",
"toUpdateYourUsername": {
"link": "Kontoeinstellungen",
"text": "Der Nutzername kann unter {link} angepasst werden. "
},
"updateUsernameNotice": "Ein Ändern des Benutzernamens aktualisiert auch deinen Link. Alle alten Links werden dann nicht mehr funktionieren.",
"videoLinkNotice": "Der Videolink verwendet denselben Link für alle Veranstaltungen, die im Rahmen deines Zeitplans erstellt werden. Bei der Erstellung von Zoom-Meetings wird für jede erstellte Veranstaltung ein neuer Zoom-Link erstellt."
"videoLinkNotice": "Der Videolink verwendet denselben Link für alle Veranstaltungen, die im Rahmen deines Zeitplans erstellt werden. Bei der Erstellung von Zoom-Meetings wird für jede erstellte Veranstaltung ein neuer Zoom-Link erstellt.",
"yourQuickLinkIs": "Dein Kurzlink ist:\n{url}"
},
"units": {
"minutes": "{value} Minuten",
Expand Down
14 changes: 12 additions & 2 deletions frontend/src/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,7 @@
"assign": "Assign",
"attendees": "Attendees",
"availabilityDay": "Availability Day",
"availableDays": "Days",
"availableDays": "Select days",
"back": "Back",
"bookEvent": "Book",
"booked": "Booked",
Expand Down Expand Up @@ -207,6 +207,7 @@
"earliestBooking": "Earliest Booking",
"edit": "Edit",
"editCalendar": "Edit calendar",
"editInSettings": "Edit in settings",
"editProfile": "Edit profile",
"email": "Email",
"end": "End",
Expand Down Expand Up @@ -257,6 +258,7 @@
"preferredEmail": "Preferred Email",
"primaryTimeZone": "Primary time zone",
"privacy": "Privacy",
"quickLink": "Quick link",
"refresh": "Refresh",
"refreshLink": "Refresh link",
"remove": "Remove",
Expand All @@ -271,6 +273,7 @@
"scheduleDetails": "Scheduling Details",
"scheduleSettings": {
"availabilitySubHeading": "Set your availability days and times",
"bookingSettingsSubHeading": "Edit your quick link and more",
"meetingDetailsSubHeading": "Add video and/or notes to events",
"schedulingDetailsSubHeading": "Set booking intervals and duration"
},
Expand Down Expand Up @@ -362,6 +365,8 @@
"text": {
"accountDataNotice": "Download all of your data from Thunderbird Appointment.",
"accountDeletionWarning": "Your account and data on Thunderbird Appointment will be deleted. This does not impact your linked calendars. And you can create a new account with us anytime.",
"bookingsWillAutomaticallyBeConfirmed": "Bookings will automatically be confirmed and added to your calendar.",
"bookingsWillRequireToBeConfirmed": "You will need to confirm or decline bookings on your calendar.",
"calendarDeletionWarning": "Removing this calendar will remove all appointments and schedules from Thunderbird Appointment. Any confirmed events currently stored in your calendar will not be removed.",
"chooseDateAndTime": "Choose when to meet.",
"connectZoom": "Connect your Zoom account to generate instant meeting invites for each booking.",
Expand Down Expand Up @@ -438,8 +443,13 @@
},
"timesAreDisplayedInLocalTimezone": "Times are displayed in your local timezone {timezone}.",
"titleIsReadyForBookings": "{title} is ready for bookings",
"toUpdateYourUsername": {
"link": "Account Settings",
"text": "To update your username, go to {link}. "
},
"updateUsernameNotice": "Changing your username will also change your link. Your old link will no longer work.",
"videoLinkNotice": "Video link will use the same link for all events created in this general availability. Generating zoom meetings will create a new zoom link for each event created."
"videoLinkNotice": "Video link will use the same link for all events created in this general availability. Generating zoom meetings will create a new zoom link for each event created.",
"yourQuickLinkIs": "Your quick link is:\n{url}"
},
"units": {
"minutes": "{value} minutes",
Expand Down
Loading

0 comments on commit 7180ead

Please sign in to comment.