Skip to content

Commit

Permalink
feat(TimeSlot): multiselect days slots (#56)
Browse files Browse the repository at this point in the history
* feat(TimeSlot): multiselect days slots

* slots UI

---------

Co-authored-by: Daniel Dobkowski <[email protected]>
  • Loading branch information
dobeck and Daniel Dobkowski authored Oct 2, 2023
1 parent ec2615f commit 3cec4b8
Show file tree
Hide file tree
Showing 23 changed files with 163 additions and 107 deletions.
7 changes: 4 additions & 3 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
"@tabler/icons": "^1.84.0",
"@trivago/prettier-plugin-sort-imports": "^4.1.1",
"axios": "^1.2.1",
"caniuse-lite": "^1.0.30001534",
"countries-and-timezones": "^3.3.0",
"date-fns": "^2.28.0",
"date-fns-tz": "^1.3.5",
Expand Down
3 changes: 1 addition & 2 deletions src/components/ContextButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,7 @@ export const ContextButton = styled.button<{ colorType: "primary" | "danger" }>`
${({ theme, colorType, disabled }) => {
const color = colorType === "danger" ? theme.colors.error : theme.colors.primary;
const borderColor =
colorType === "danger" ? theme.colors.error : theme.colorSchemas.timeSlotButton.available.border;
const borderColor = colorType === "danger" ? theme.colors.error : theme.colorSchemas.input.border;
const borderColorHover = colorType === "danger" ? theme.colors.error : theme.colors.darkGrey;
return css`
Expand Down
2 changes: 1 addition & 1 deletion src/components/EventsWrapper/components/EventSlot.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ const EventSlotButton = styled.button<EventSlotButtonProps>`
return css`
color: ${colorSchema.text};
cursor: ${state === "unavailable" ? "unset" : "pointer"};
border-color: ${colorSchema.border};
border: 1px solid ${colorSchema.border};
border-radius: ${({ theme }) => theme.borderRadius};
background-color: ${colorSchema.background};
`;
Expand Down
2 changes: 1 addition & 1 deletion src/components/layout/AppLayoutWrapper.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export const Wrapper = styled.div`
display: grid;
grid-template-columns:
1fr
min(900px, calc(100% - 40px))
min(980px, calc(100% - 40px))
1fr;
& > * {
Expand Down
43 changes: 22 additions & 21 deletions src/features/service/components/Service/BookService/BookService.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { Column } from "components/layout/Column";
import { useBookDateRange } from "features/service/hooks/useBookDateRange";
import { useBookSlot } from "features/service/hooks/useBookSlot";
import { Form, Formik } from "formik";
import { getServiceConfigByType } from "helpers/functions";
import { useLocale } from "helpers/hooks/useLocale";
import { convertSourceDateTimeToTargetDateTime } from "helpers/timeFormat";
import _ from "lodash";
Expand All @@ -15,16 +16,15 @@ import { BOOKING_FORM_TYPES } from "models/service";
import { useTranslation } from "react-i18next";
import { useSearchParams } from "react-router-dom";
import { useParams } from "react-router-dom";
import { useRecoilValue } from "recoil";
import { useRecoilState, useRecoilValue } from "recoil";
import { hoursSystemAtom } from "state/atoms";
import { selectedDateRange } from "state/atoms/selectedDateRange";
import { selectedSlot } from "state/atoms/selectedSlot";
import { selectedSlots } from "state/atoms/selectedSlots";
import { serviceAtom } from "state/atoms/service";
import { slotsAtom } from "state/atoms/slots";
import { slotsFiltersAtom } from "state/atoms/slotsFilters";
import { timeZoneAtom } from "state/atoms/timeZone";
import { uploadAttachmentsAtom } from "state/atoms/uploadAttachments";
import { selectedSlotSelector } from "state/selectors/selectedSlotSelector";
import styled from "styled-components";
import { IconInfoCircle } from "@tabler/icons";
import { BookingServiceFormContent } from "../BookingServiceFormContent/BookingServiceFormContent";
Expand Down Expand Up @@ -84,40 +84,41 @@ const BookService = () => {
const [searchParams] = useSearchParams();
const locale = useLocale();
const { t } = useTranslation(["forms"]);
const selectedSlotValue = useRecoilValue(selectedSlot);
const selectedDateRangeValue = useRecoilValue(selectedDateRange);
const selectedSlotsValue = useRecoilValue(selectedSlots);
const service = useRecoilValue(serviceAtom)!;
const serviceType = service?.viewConfig.displayType;
const serviceConfig = service && getServiceConfigByType({ service });
const { formFields }: { formFields: Array<FormField> } = service ?? {
formFields: [],
};
const showWarning = useRecoilValue(slotsFiltersAtom).triggerId !== 0;
const slot = useRecoilValue(selectedSlotSelector);
const { bookSlotMutation, loading, error } = useBookSlot();
const { bookDateRangeMutation, loadingDateRange, errorDateRange } = useBookDateRange();
const { id } = useParams<{ id: string }>();
const uploadState = useRecoilValue(uploadAttachmentsAtom);
const timeZone = useRecoilValue(timeZoneAtom);
const hoursSystem = useRecoilValue(hoursSystemAtom);
const is12HoursSystem = useMemo(() => hoursSystem === HOURS_SYSTEMS.h12, [hoursSystem]);
const [, setSelectedSlots] = useRecoilState(selectedSlots);
const slots = useRecoilValue(slotsAtom)!;

const isUploading = Object.values(uploadState).filter((item) => item.isLoading).length > 0;

const dateFormat = is12HoursSystem ? "iiii dd MMM, h:mm a" : "iiii dd MMM, H:mm";

const formattedDate =
selectedSlotValue &&
convertSourceDateTimeToTargetDateTime({
date: selectedSlotValue,
targetTimeZone: timeZone,
sourceTimeZone: service.project.localTimeZone,
dateFormat,
locale,
});
const selectedSlot = slots.find((slot) => slot.slotId === selectedSlotsValue[0])!;
const formattedDate = selectedSlotsValue?.length
? convertSourceDateTimeToTargetDateTime({
date: selectedSlot.dateTimeFrom,
targetTimeZone: timeZone,
sourceTimeZone: service.project.localTimeZone,
dateFormat,
locale,
})
: "";

const checkDisableButton = useCallback(() => {
const disabledForSlot = selectedSlotValue === "" || loading || isUploading;
const disabledForSlots = !selectedSlotsValue.length || loading || isUploading;
const disabledForDateRange =
selectedDateRangeValue.dateTimeFrom === null ||
Expand All @@ -129,10 +130,9 @@ const BookService = () => {
const isEventType = serviceType === BOOKING_FORM_TYPES.LIST;

return (
(isSlotType && disabledForSlot) || (isDateRangeType && disabledForDateRange) || (isEventType && disabledForSlots)
(isSlotType && disabledForSlots) || (isDateRangeType && disabledForDateRange) || (isEventType && disabledForSlots)
);
}, [
selectedSlotValue,
loading,
isUploading,
selectedDateRangeValue.dateTimeFrom,
Expand Down Expand Up @@ -170,19 +170,19 @@ const BookService = () => {
...Object.assign({}, ...customFormFields),
});

if (serviceType === BOOKING_FORM_TYPES.DAYS && slot !== undefined) {
if (serviceType === BOOKING_FORM_TYPES.DAYS && selectedSlotsValue.length) {
bookSlotMutation({
variables: {
serviceId: id!,
slots: [slot.slotId],
slots: selectedSlotsValue,
formFields: json,
timezone: timeZone,
...(service?.paymentProviders.length && {
paymentProvider: service.paymentProviders[0],
}),
locale: locale.code,
},
});
}).then(() => setSelectedSlots([]));
} else if (
serviceType === BOOKING_FORM_TYPES.CALENDAR &&
selectedDateRangeValue.dateTimeFrom !== null &&
Expand Down Expand Up @@ -212,7 +212,7 @@ const BookService = () => {
}),
locale: locale.code,
},
});
}).then(() => setSelectedSlots([]));
}
};

Expand Down Expand Up @@ -250,6 +250,7 @@ const BookService = () => {
selectedSlotValue: formattedDate,
selectedSlotsValue,
t,
serviceConfig,
})}
</Button>
</Column>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,20 +1,32 @@
import { Service } from "models/service";
import { TFunction } from "react-i18next";

type GetSubmitButtonText = ({
selectedSlotValue,
selectedSlotsValue,
t,
serviceConfig,
}: {
selectedSlotValue: string;
selectedSlotsValue: string[];
t: TFunction<"forms"[]>;
serviceConfig: Service["viewConfig"]["days" | "list" | "calendar"];
}) => string;

export const getSubmitButtonText: GetSubmitButtonText = ({ selectedSlotValue, selectedSlotsValue, t }) => {
export const getSubmitButtonText: GetSubmitButtonText = ({
selectedSlotValue,
selectedSlotsValue,
t,
serviceConfig,
}) => {
const textBase = t("book-free-button");
const isMultiSelect = serviceConfig.multiSelect;

if (selectedSlotValue === "" && !selectedSlotsValue.length) return textBase;
if (selectedSlotValue === "" && selectedSlotsValue.length) return `${textBase} (${selectedSlotsValue.length})`;
if (selectedSlotValue !== "" && selectedSlotsValue.length) {
if (isMultiSelect) return `${textBase} (${selectedSlotsValue.length})`;
return `${textBase}: ${selectedSlotValue}`;
}

return `${textBase}: ${selectedSlotValue}`;
};
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { Service } from "models/service";
import { getSubmitButtonText } from "../";

describe("getSubmitButtonText", () => {
Expand All @@ -8,21 +9,28 @@ describe("getSubmitButtonText", () => {
return "";
};

const serviceConfigMock = {
multiSelect: false,
} as Service["viewConfig"]["days" | "list" | "calendar"];

it("returns base text when no selectedSlotValue and selectedSlotsValue", () => {
const buttonText = getSubmitButtonText({
selectedSlotValue: "",
selectedSlotsValue: [],
t: tMock,
serviceConfig: serviceConfigMock,
});

expect(buttonText).toBe("Book now");
});

it("returns text with selectedSlotsValue length when no selectedSlotValue", () => {
serviceConfigMock.multiSelect = true;
const buttonText = getSubmitButtonText({
selectedSlotValue: "",
selectedSlotValue: "slot1",
selectedSlotsValue: ["slot1", "slot2"],
t: tMock,
serviceConfig: serviceConfigMock,
});

expect(buttonText).toBe("Book now (2)");
Expand All @@ -33,6 +41,7 @@ describe("getSubmitButtonText", () => {
selectedSlotValue: "slot1",
selectedSlotsValue: [],
t: tMock,
serviceConfig: serviceConfigMock,
});

expect(buttonText).toBe("Book now: slot1");
Expand Down
32 changes: 13 additions & 19 deletions src/features/service/components/Service/HoursSystem/HoursSystem.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,20 @@
import React from "react";
import { Typography } from "components/Typography";
import { Row } from "components/layout/Row";
import { useTranslation } from "react-i18next";
import { useRecoilValue, useSetRecoilState } from "recoil";
import { hoursSystemAtom } from "state/atoms";
import styled, { css } from "styled-components";
import { HOURS_SYSTEMS } from "./enums/HoursSystem.enum";

const Wrapper = styled(Row)`
gap: 4px;
border-radius: 4px;
cursor: pointer;
padding: 3px 6px;
&:hover {
background-color: ${({ theme }) => theme.colors.primaryLight};
}
`;

const HoursSystemButton = styled(Typography)<{ isBold: boolean }>`
Expand All @@ -17,50 +23,38 @@ const HoursSystemButton = styled(Typography)<{ isBold: boolean }>`
${({ isBold }) => {
return css`
font-weight: ${isBold ? "700" : "normal"};
cursor: ${isBold ? "unset" : "pointer"};
&:hover {
text-decoration: ${isBold ? "none" : "underline"};
}
text-decoration: ${isBold ? "underline" : "none"};
`;
}}
`;

export const HoursSystem = () => {
const { t } = useTranslation();
const hoursSystem = useRecoilValue(hoursSystemAtom);
const setHoursSystem = useSetRecoilState(hoursSystemAtom);

const handleHoursSystemChange = (hoursSystem: string) => {
localStorage.setItem("HOURS_SYSTEM", hoursSystem);
setHoursSystem(hoursSystem);
const handleHoursSystemChange = () => {
const newHoursSystem = hoursSystem === HOURS_SYSTEMS.h12 ? HOURS_SYSTEMS.h24 : HOURS_SYSTEMS.h12;
localStorage.setItem("HOURS_SYSTEM", newHoursSystem);
setHoursSystem(newHoursSystem);
};

return (
<Wrapper>
<Typography className="timezone-info" typographyType="label" color="inherit" as="span">{`${t(
"format",
)}:`}</Typography>
<Wrapper onClick={handleHoursSystemChange}>
<HoursSystemButton
className="timezone-info"
typographyType="label"
color="inherit"
as="span"
isBold={hoursSystem === HOURS_SYSTEMS.h12}
onClick={() => handleHoursSystemChange(HOURS_SYSTEMS.h12)}
>
12h
</HoursSystemButton>
<Typography className="timezone-info" typographyType="label" color="inherit" as="span">
|
</Typography>
<HoursSystemButton
className="timezone-info"
typographyType="label"
color="inherit"
as="span"
isBold={hoursSystem === HOURS_SYSTEMS.h24}
onClick={() => handleHoursSystemChange(HOURS_SYSTEMS.h24)}
>
24h
</HoursSystemButton>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { Typography } from "components/Typography";
import { Box } from "components/layout/Box";
import { useLangParam } from "features/i18n/useLangParam";
import { useRescheduleBooking } from "features/service/hooks/useRescheduleBooking";
import { getPath } from "helpers/functions";
import { getPath, getServiceConfigByType } from "helpers/functions";
import { useLocale } from "helpers/hooks/useLocale";
import { convertSourceDateTimeToTargetDateTime } from "helpers/timeFormat";
import { BOOKING_FORM_TYPES } from "models/service";
Expand Down Expand Up @@ -53,6 +53,7 @@ const RescheduleService = () => {
const selectedDateRangeValue = useRecoilValue(selectedDateRange);
const selectedSlotsValue = useRecoilValue(selectedSlots);
const service = useRecoilValue(serviceAtom)!;
const serviceConfig = service && getServiceConfigByType({ service });
const slot = useRecoilValue(selectedSlotSelector);
const { rescheduleBookingMutation, loading } = useRescheduleBooking();
const timeZone = useRecoilValue(timeZoneAtom);
Expand Down Expand Up @@ -176,6 +177,7 @@ const RescheduleService = () => {
selectedSlotValue: formattedDateTo,
selectedSlotsValue,
t,
serviceConfig,
})}
</Button>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,13 @@ const PaginationButton = styled(ContextButton)`
height: 36px;
width: 36px;
display: grid;
border: none;
&:hover {
border: none;
background: ${({ theme, disabled }) =>
disabled ? "transparent" : theme.colorSchemas.timeSlotButton.available.background};
}
& > * {
margin: auto;
${({ theme, disabled }) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,6 @@ const ServiceCalendarDay: React.FC<ServiceCalendarDayProps> = ({ day }) => {
key={`${dayPart}-${item.key}`}
dateFrom={`${dayPart}T${item.from}`}
dateTo={`${dayPart}T${item.to}`}
day={day}
is12HoursSystem={is12HoursSystem}
/>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@ const ServiceCalendarWrapper = () => {
const [serviceCalendarFilters, setServiceCalendarFilters] = useRecoilState(slotsFiltersAtom);
const { t } = useTranslation();
const slotsViewConfig = useRecoilValue(slotsViewConfiguration);

const pageSize = Math.min(slotsViewConfig.maxDaysPerPage, Math.trunc(width / slotsViewConfig.slotsColumnWidth));
const daysToRender = useMemo(
() => days.slice(0, pageSize === 0 ? slotsViewConfig.minDaysPerPage : pageSize),
Expand Down
Loading

0 comments on commit 3cec4b8

Please sign in to comment.