Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More
Socket
Sign inDemoInstall
Socket

cronofy-elements

Package Overview
Dependencies
Maintainers
3
Versions
172
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

cronofy-elements - npm Package Compare versions

Comparing version 1.16.1 to 1.17.0

build/CronofyElements.v1.17.0.js

9

git.README.md

@@ -15,2 +15,5 @@ # Cronofy UI Elements

You will need to ensure `/src/js/env.js` is updated with your local development settings. See `/src/js/env.example.js` for an example of this.
To start development, run:

@@ -20,2 +23,6 @@

If you're running SSL on your local dev server then you will need to tell Node to ignore TLS verifications.
NODE_TLS_REJECT_UNAUTHORIZED=0 make dev
This will watch the source files and rebuild whenever you save changes. It will also host the demo app (`demo/index.ejs`) on port `8080`

@@ -33,3 +40,3 @@

To bump a patch version (e.g. from `0.0.1` to `0.0.2`) and make a production build, run:
bump_patch

@@ -36,0 +43,0 @@

2

package.json
{
"name": "cronofy-elements",
"version": "1.16.1",
"version": "1.17.0",
"description": "Fast track scheduling with Cronofy's embeddable UI Elements",

@@ -5,0 +5,0 @@ "main": "build/npm/CronofyElements.js",

@@ -9,4 +9,3 @@ import React, { useState, useEffect } from "react";

getSlotsForWeek,
calculateSlotHeight,
parseInterval
calculateSlotHeight
} from "../../helpers/slots";

@@ -16,5 +15,7 @@ import { objectIsEmpty, uniqueItems } from "../../helpers/utils";

addKeysToSlots,
checkSelectedState,
cropQuery,
generateWeeklySlots,
generateStaticWeeks
generateStaticWeeks,
removeManagedAvailability
} from "../../helpers/utils.AvailabilityViewer";

@@ -57,3 +58,3 @@ import { parseStyleOptions } from "../../helpers/theming";

: 60,
interval: parseInterval(options.config.interval)
interval: options.config.interval
}),

@@ -84,3 +85,2 @@ sizes: {

: false,
multiSelect: options.config.mode === "multi_select" || false,
preloading: false,

@@ -114,3 +114,3 @@ notificationCallback: options.callback

end: options.config.end_time ? options.config.end_time : "17:30",
interval: parseInterval(options.config.interval)
interval: options.config.interval
});

@@ -262,3 +262,3 @@

if (!error) {
const query = {
let query = {
...options.query,

@@ -269,2 +269,6 @@ response_format: "overlapping_slots",

if (status.mode === "free_select") {
query = removeManagedAvailability(query);
}
const croppedQuery = cropQuery(query, {

@@ -336,3 +340,9 @@ currentWeek: weeks.current,

});
setSlotData(slots.slots);
let updatedSlots = slots.slots;
if (status.mode === "free_select") {
// If we're overriding the slots state (with `setSlotData`), then
// We'll need to make sure we persit any "selected" slots.
updatedSlots = checkSelectedState(slots.slots, slotData);
}
setSlotData(updatedSlots);
}

@@ -339,0 +349,0 @@ }, [limits, rawData]);

@@ -9,3 +9,3 @@ import React, { useContext } from "react";

const DayColumn = ({ day, columnCount }) => {
const DayColumn = ({ day, columnCount, toggleMultiple = false }) => {
const [theme, setTheme] = useContext(ThemeContext);

@@ -28,2 +28,3 @@ return (

stylePrefix={theme.prefix}
toggleMultiple={toggleMultiple}
/>

@@ -30,0 +31,0 @@ ) : (

@@ -7,3 +7,3 @@ import React, { useContext } from "react";

const LegendItem = ({ available }) => {
const LegendItem = ({ status }) => {
const [theme, setTheme] = useContext(ThemeContext);

@@ -27,12 +27,8 @@ const i18n = useContext(I18nContext);

margin-right: 10px;
background: ${available
? theme.colors.available
: theme.colors.unavailable};
background: ${theme.colors[status]};
`}
className={`${theme.prefix}__example ${
theme.prefix
}__example--${available ? "available" : "unavailable"}`}
className={`${theme.prefix}__example ${theme.prefix}__example--${status}`}
/>
<span className={`${theme.prefix}__legend-label`}>
{available ? i18n.t("available") : i18n.t("unavailable")}
{i18n.t(status)}
</span>

@@ -57,4 +53,5 @@ </div>

>
<LegendItem available={true} />
<LegendItem available={false} />
<LegendItem status={"available"} />
<LegendItem status={"unavailable"} />
{status.mode === "free_select" && <LegendItem status={"booked"} />}
{status.customtzid ? <TimeZoneDisplay /> : null}

@@ -61,0 +58,0 @@ </div>

@@ -5,6 +5,8 @@ import React, { useContext } from "react";

import Slot from "./Slot";
import SlotFreeSelect from "./SlotFreeSelect";
import { ThemeContext } from "./AvailabilityViewer";
import { StatusContext, ThemeContext } from "./AvailabilityViewer";
const Overlay = ({ day, columnCount }) => {
const Overlay = ({ day, columnCount, toggleMultiple }) => {
const [status, setStatus] = useContext(StatusContext);
const [theme, setTheme] = useContext(ThemeContext);

@@ -28,9 +30,21 @@

>
<Slot
slot={slot}
day={day}
columnCount={columnCount}
topSlot={topSlot}
blockBelow={blockBelow}
/>
{status.mode !== "free_select" && (
<Slot
slot={slot}
day={day}
columnCount={columnCount}
topSlot={topSlot}
blockBelow={blockBelow}
/>
)}
{status.mode === "free_select" && (
<SlotFreeSelect
slot={slot}
day={day}
columnCount={columnCount}
topSlot={topSlot}
blockBelow={blockBelow}
toggleMultiple={toggleMultiple}
/>
)}
</div>

@@ -37,0 +51,0 @@ );

@@ -1,5 +0,4 @@

import React, { useContext } from "react";
import moment from "moment-timezone";
import React, { useContext, useState } from "react";
import { css, jsx } from "@emotion/core";
import { darken, transparentize } from "polished";
import { transparentize } from "polished";

@@ -9,3 +8,6 @@ import Tooltip from "./Tooltip";

import { filterObject } from "../../helpers/utils";
import { createExportableSelection } from "../../helpers/utils.AvailabilityViewer";
import {
createExportableSelection,
calculateDisplayText
} from "../../helpers/utils.AvailabilityViewer";
import { buttonReset } from "../../styles/utils";

@@ -23,13 +25,6 @@

const startObject = moment.tz(
slot.start,
"YYYY-MM-DDTTHH:mm:00Z",
status.tzid
const [displayText, setDisplayText] = useState(() =>
calculateDisplayText(i18n, slot, status)
);
const endObject = moment.tz(slot.end, "YYYY-MM-DDTTHH:mm:00Z", status.tzid);
const startDisplay = i18n.f(startObject, "LT").replace(" ", "");
const endDisplay = i18n.f(endObject, "LT").replace(" ", "");
const timeDisplay = `${startDisplay} - ${endDisplay}`;
const removeFromSelectionList = () => {

@@ -39,3 +34,3 @@ const filteredSlots = filterObject(slot.start, selectedSlots);

const exportableSelection = createExportableSelection(filteredSlots);
const callbackContent = {
let callbackContent = {
notification: {

@@ -58,4 +53,10 @@ type: "slot_removed",

background: ${theme.colors.availableActive};
border: 1px solid ${darken(0.05, theme.colors.availableActive)};
box-shadow: ${transparentize(0.9, theme.colors.black)} 0 5px 5px -2px;
${
status.mode !== "free_select"
? `box-shadow: ${transparentize(
0.9,
theme.colors.black
)} 0 5px 5px -2px;`
: ""
}
&:hover .${theme.prefix}__slot-tooltip {

@@ -68,5 +69,7 @@ opacity: 1;

>
{!status.multiSelect && status.mode !== "no_confirm" ? (
{status.mode !== "multi_select" &&
status.mode !== "no_confirm" &&
status.mode !== "free_select" ? (
<Tooltip
time={timeDisplay}
time={displayText.time}
top={slotDetails.topSlot}

@@ -81,3 +84,5 @@ hover={true}

${buttonReset};
background: ${theme.colors.availableActive};
background: ${status.mode === "free_select"
? theme.colors.available
: theme.colors.availableActive};
width: 100%;

@@ -87,6 +92,10 @@ height: 100%;

color: ${theme.colors.white};
&:focus {
outline: none;
}
`}
className={`${theme.prefix}__selected-slot__button`}
onClick={() =>
status.multiSelect
status.mode === "multi_select" ||
status.mode === "free_select"
? removeFromSelectionList()

@@ -101,5 +110,3 @@ : status.notificationCallback({

>
{status.mode === "multi_select" || status.mode === "no_confirm"
? startDisplay
: i18n.t("confirm")}
{displayText.text}
</button>

@@ -106,0 +113,0 @@ </div>

@@ -32,6 +32,6 @@ import React, { useState, useContext, useEffect } from "react";

const defaultTimeDisplayStart = i18n
.f(moment(slot.start, "YYYY-MM-DDTTHH:mm:00Z").tz(status.tzid), "LT")
.f(moment(slot.start, "YYYY-MM-DDTHH:mm:00Z").tz(status.tzid), "LT")
.replace(" ", "");
const defaultTimeDisplayEnd = i18n
.f(moment(slot.end, "YYYY-MM-DDTTHH:mm:00Z").tz(status.tzid), "LT")
.f(moment(slot.end, "YYYY-MM-DDTHH:mm:00Z").tz(status.tzid), "LT")
.replace(" ", "");

@@ -94,7 +94,7 @@

slotData.slot.start,
"YYYY-MM-DDTTHH:mm:00Z"
"YYYY-MM-DDTHH:mm:00Z"
).tz(status.tzid);
const timeDisplayStart = i18n.f(timeStart, "LT").replace(" ", "");
const timeEnd = moment(slotData.slot.end, "YYYY-MM-DDTTHH:mm:00Z").tz(
const timeEnd = moment(slotData.slot.end, "YYYY-MM-DDTHH:mm:00Z").tz(
status.tzid

@@ -164,3 +164,3 @@ );

// If we're in multi_select mode,
if (status.multiSelect) {
if (status.mode === "multi_select") {
const exportableSelection = createExportableSelection(

@@ -167,0 +167,0 @@ newSlots

import React, { useContext } from "react";
import { css, jsx } from "@emotion/core";
import { ThemeContext } from "./AvailabilityViewer";
import { ThemeContext, StatusContext } from "./AvailabilityViewer";
const Slots = ({ day }) => {
const [theme, setTheme] = useContext(ThemeContext);
const slotsMarkup = day.slots.map((slot, i) => (
<div
css={css`
display: block;
height: ${theme.slotHeightCalc.height}px;
position: relative;
const [status, setStatus] = useContext(StatusContext);
${slot.visiblyAvailable
? `background: ${theme.colors.available};`
: ""}
`}
className={`${theme.prefix}__slot-background ${
theme.prefix
}__slot-background--${
slot.visiblyAvailable ? "available" : "unavailable"
}`}
key={i}
/>
));
const slotsMarkup = day.slots.map((slot, i) => {
let diagonal = Math.sqrt(2 * Math.pow(theme.slotHeightCalc.height, 2));
let colour =
status.mode !== "free_select" && slot.visiblyAvailable
? theme.colors.available
: "";
if (status.mode === "free_select" && slot.selected) {
colour = theme.colors.available;
}
if (status.mode === "free_select" && !slot.visiblyAvailable) {
colour = theme.colors.booked;
}
return (
<div
css={css`
display: block;
height: ${theme.slotHeightCalc.height}px;
position: relative;
background: ${colour};
${status.mode === "free_select" &&
!slot.visiblyAvailable &&
slot.selected
? `
background: none;
background-size: ${theme.slotHeightCalc.height}px ${
theme.slotHeightCalc.height
}px;
background-image: repeating-linear-gradient(
to bottom right,
${theme.colors.available},
${theme.colors.available} ${diagonal / 4}px,
${theme.colors.booked} ${diagonal / 4}px,
${theme.colors.booked} ${diagonal / 2}px
);`
: ``}
`}
className={`${theme.prefix}__slot-background ${
theme.prefix
}__slot-background--${
slot.visiblyAvailable ? "available" : "unavailable"
}`}
key={i}
/>
);
});

@@ -28,0 +56,0 @@ return slotsMarkup;

@@ -7,2 +7,3 @@ import React, { useContext, useState, useEffect } from "react";

import DayColumnDisplay from "./DayColumnDisplay";
import DayColumnWrapper from "./DayColumnWrapper";
import Error from "../generic/Error";

@@ -25,3 +26,7 @@ import HoverSlot from "./HoverSlot";

} from "./AvailabilityViewer";
import { checkBlockedSlots } from "../../helpers/utils.AvailabilityViewer";
import {
buildPeriodsFromSlots,
checkBlockedSlots,
getSelectedSlots
} from "../../helpers/utils.AvailabilityViewer";
import { objectIsEmpty } from "../../helpers/utils";

@@ -31,24 +36,4 @@

export const SelectionContext = React.createContext();
export const DragContext = React.createContext();
const DayColumnWrapper = ({ children, layer = 1 }) => {
const [theme, setTheme] = useContext(ThemeContext);
return (
<div
css={css`
display: flex;
justify-content: stretch;
position: absolute;
z-index: ${layer};
top: 0;
left: 0;
width: 100%;
height: 100%;
`}
className={`${theme.prefix}__grid-columns`}
>
{children}
</div>
);
};
const Week = () => {

@@ -62,2 +47,9 @@ const [limits, setLimits] = useContext(LimitsContext);

const [drag, setDrag] = useState({
mouseDown: false,
toggleState: "available",
dragged: [],
dragStart: false
});
const [hover, setHover] = useState({

@@ -82,2 +74,37 @@ times: false,

const toggleMultiple = (IDs, toggleStatus) => {
const updatedSlotData = slotData.map(day => ({
...day,
slots: day.slots.map(slot =>
IDs.includes(slot.start)
? { ...slot, selected: toggleStatus }
: slot
)
}));
const selectedSlots = getSelectedSlots(updatedSlotData);
const queryPeriods = buildPeriodsFromSlots(selectedSlots);
const callbackContent = {
notification: {
type: "query_periods_edited",
query_periods: queryPeriods
}
};
status.notificationCallback(callbackContent);
setSlotData(updatedSlotData);
};
const handleMouseOut = () => {
if (drag.mouseDown) {
toggleMultiple(drag.dragged, drag.toggleState);
setDrag({
mouseDown: false,
toggleState: true,
dragged: [],
dragStart: false
});
}
};
useEffect(() => {

@@ -99,3 +126,3 @@ if (

if (objectIsEmpty(selectedSlots)) {
if (objectIsEmpty(selectedSlots) && status.mode !== "free_select") {
hoverElement.current.style.display = "none";

@@ -107,7 +134,11 @@ hoverElementTooltip.current.style.display = "none";

useEffect(() => {
hoverElement.current.style.display = "flex";
hoverElement.current.style.opacity = hover.visible ? "1" : "0";
if (status.mode !== "free_select") {
hoverElement.current.style.display = "flex";
hoverElement.current.style.opacity = hover.visible ? "1" : "0";
hoverElementTooltip.current.style.display = "flex";
hoverElementTooltip.current.style.opacity = hover.visible ? "1" : "0";
hoverElementTooltip.current.style.display = "flex";
hoverElementTooltip.current.style.opacity = hover.visible
? "1"
: "0";
}
}, [hover]);

@@ -118,2 +149,4 @@

? hover.times
: status.mode === "free_select"
? i18n.t("booked")
: i18n.t("unavailable");

@@ -135,3 +168,3 @@

useEffect(() => {
if (!status.multiSelect) {
if (status.mode !== "multi_select") {
setSelectedSlots({});

@@ -170,2 +203,3 @@ }

className={`${theme.prefix}__grid`}
onMouseLeave={handleMouseOut}
>

@@ -180,54 +214,18 @@ {!status.loading ? (

<TimeLines />
<div
ref={hoverElement}
css={css`
position: absolute;
z-index: 2;
display: none;
width: ${theme.sizes.columnWidth}px;
height: ${hoverHeight}px;
top: ${hover.position.y}px;
left: ${hover.position.x *
theme.sizes.columnWidth}px;
`}
className={`${theme.prefix}__hover-positioner ${theme.prefix}__hover-positioner--main`}
>
<HoverSlot
available={hover.available}
times={tooltipText}
topSlot={hover.topSlot}
/>
</div>
<DayColumnWrapper layer={4}>
{slots.map((day, columnCount) => (
<DayColumn
key={day.day}
columnCount={columnCount}
day={day}
/>
))}
</DayColumnWrapper>
{!objectIsEmpty(selectedSlots) &&
status.mode === "confirm" ? (
<SelectionMask />
) : null}
<SelectedSlots bounds={[slots[0].day, slots[6].day]} />
<div
ref={hoverElementTooltip}
css={css`
position: absolute;
z-index: 5;
display: none;
pointer-events: none;
user-select: none;
top: ${hover.position.y}px;
left: ${hover.position.x *
theme.sizes.columnWidth}px;
width: ${theme.sizes.columnWidth}px;
height: ${hoverHeight}px;
`}
className={`${theme.prefix}__hover-positioner ${theme.prefix}__hover-positioner--tooltip`}
>
{!hover.hideTooltip ? (
<HoverTooltip
{status.mode !== "free_select" && (
<div
ref={hoverElement}
css={css`
position: absolute;
z-index: 2;
display: none;
width: ${theme.sizes.columnWidth}px;
height: ${hoverHeight}px;
top: ${hover.position.y}px;
left: ${hover.position.x *
theme.sizes.columnWidth}px;
`}
className={`${theme.prefix}__hover-positioner ${theme.prefix}__hover-positioner--main`}
>
<HoverSlot
available={hover.available}

@@ -237,4 +235,54 @@ times={tooltipText}

/>
) : null}
</div>
</div>
)}
<DayColumnWrapper layer={4}>
<DragContext.Provider value={[drag, setDrag]}>
{slots.map((day, columnCount) => (
<DayColumn
key={day.day}
columnCount={columnCount}
day={day}
toggleMultiple={toggleMultiple}
/>
))}
</DragContext.Provider>
</DayColumnWrapper>
{status.mode !== "free_select" && (
<React.Fragment>
{!objectIsEmpty(selectedSlots) &&
status.mode === "confirm" ? (
<SelectionMask />
) : null}
<SelectedSlots
bounds={[slots[0].day, slots[6].day]}
/>
<div
ref={hoverElementTooltip}
css={css`
position: absolute;
z-index: 5;
display: none;
pointer-events: none;
user-select: none;
top: ${hover.position.y}px;
left: ${hover.position.x *
theme.sizes.columnWidth}px;
width: ${theme.sizes.columnWidth}px;
height: ${hoverHeight}px;
`}
className={`${theme.prefix}__hover-positioner ${theme.prefix}__hover-positioner--tooltip`}
>
{!hover.hideTooltip ? (
<HoverTooltip
available={hover.available}
times={tooltipText}
topSlot={hover.topSlot}
/>
) : null}
</div>
</React.Fragment>
)}
{status.loading ? <Loading /> : null}

@@ -241,0 +289,0 @@ {status.preloading ? <Preloading /> : null}

@@ -80,3 +80,3 @@ import React, { useContext } from "react";

{i18n.f(
moment(slots.day, "YYYY-MM-DDTThh:mm:00Z").tz(
moment(slots.day, "YYYY-MM-DDThh:mm:00Z").tz(
status.tzid

@@ -83,0 +83,0 @@ ),

// import React from "react";
import ReactDOM from "react-dom";
import merge from "deepmerge";

@@ -64,2 +65,3 @@ import Error from "../components/generic/Error";

const reload = newRawOptions => {
console.log("newRawOptions", newRawOptions);
// Increment the key - this will ensure the React

@@ -75,4 +77,4 @@ // component re-renders when the `options` change.

return {
update: newOptions => reload({ ...originalOptions, ...newOptions })
update: newOptions => reload(merge(originalOptions, newOptions))
};
};
import moment from "moment-timezone";
import {
parseInterval,
parseToken,

@@ -35,5 +36,5 @@ parseTarget,

const target = parseTarget(options, "availability-viewer", log);
const query = parseQuery(options, "availability-viewer", log);
if (!query) return false;
const interval = parseInterval(options.config.interval);
if (typeof options.extras !== "undefined") {

@@ -75,2 +76,27 @@ log.warn(

const query = parseQuery(options, "availability-viewer", log);
if (!query) return false;
if (config.mode === "free_select") {
query.required_duration.minutes = interval;
const now = moment.utc().endOf("hour");
// Adding 34 days minus 1 hour to ensure that the resulting
// query stays within the requiered bounds defined in
// https://docs.cronofy.com/developers/api/scheduling/availability/#param-participants.members.available_periods.end
query.query_periods = [
{
start: now.format("YYYY-MM-DDTHH:mm[:00Z]"),
end: now
.clone()
.add(35, "days")
.subtract(1, "hours")
.format("YYYY-MM-DDTHH:mm[:00Z]")
}
];
config.week_start_day = now
.tz(options.tzid)
.format("dddd")
.toLowerCase();
}
const startDay =

@@ -103,3 +129,11 @@ typeof config.week_start_day === "undefined" ||

config = { ...config, startDay, mode, boundsControl, slot_selection, logs };
config = {
...config,
interval,
startDay,
mode,
boundsControl,
slot_selection,
logs
};

@@ -106,0 +140,0 @@ const domains = parseConnectionDomains(

@@ -125,1 +125,14 @@ import merge from "deepmerge";

};
export const parseInterval = option => {
// Make sure we have an interval to start with...
let interval = option ? option : 15;
// Round it to the nearest 15 mins
interval = Math.ceil(interval / 15) * 15;
// If it's 45, bump it up to 60 [^1]
interval = interval === 45 ? 60 : interval;
// If it's over 60, round down [^1]
interval = interval > 60 ? 60 : interval;
// [:1] We currently only support the following intervals: 15 | 30 | 60
return interval;
};

@@ -318,15 +318,2 @@ const moment = require("moment-timezone");

export const parseInterval = option => {
// Make sure we have an interval to start with...
let interval = option ? option : 15;
// Round it to the nearest 15 mins
interval = Math.ceil(interval / 15) * 15;
// If it's 45, bump it up to 60 [^1]
interval = interval === 45 ? 60 : interval;
// If it's over 60, round down [^1]
interval = interval > 60 ? 60 : interval;
// [:1] We currently only support the following intervals: 15 | 30 | 60
return interval;
};
export const filterOverflowingSlots = (slots, overflow) =>

@@ -333,0 +320,0 @@ slots.filter((slot, i) => {

@@ -190,3 +190,2 @@ import moment from "moment-timezone";

export const connectSlots = (start_id, current_id) => {
// console.log("start_id, current_id", start_id, current_id);
const start_indexes = start_id.split("_");

@@ -193,0 +192,0 @@ const end___indexes = current_id.split("_");

@@ -28,7 +28,6 @@ import { darken, lighten } from "polished";

primaryLight: "#B8E8F2",
// primaryDark: "#B8E8F2",
secondary: "#CB359B",
// secondaryLight: "#B8E8F2",
// secondaryDark: "#B8E8F2",
background: "#F2F2F2",
booked: "#E3E3E3",
bookedSelected: "#DBECC9",
available: "#E2FAC8",

@@ -35,0 +34,0 @@ availableHover: "#C0E992",

@@ -189,2 +189,3 @@ import moment from "moment-timezone";

blocked: false,
selected: false,
target: false,

@@ -488,1 +489,190 @@ targetOffset: 0,

};
export const removeManagedAvailability = query => ({
...query,
participants: query.participants.map(parts => ({
...parts,
members: parts.members.map(member => {
delete member.availability_rule_ids;
delete member.managed_availability;
return member;
})
}))
});
export const calculateDisplayText = (i18n, slot, status) => {
const startObject = moment.tz(
slot.start,
"YYYY-MM-DDTHH:mm:00Z",
status.tzid
);
const endObject = moment.tz(slot.end, "YYYY-MM-DDTHH:mm:00Z", status.tzid);
const startTime = i18n.f(startObject, "LT").replace(" ", "");
const endTime = i18n.f(endObject, "LT").replace(" ", "");
let text;
switch (status.mode) {
case "multi_select":
case "no_confirm":
text = `${startTime} - ${endTime}`;
break;
case "free_select":
text = "";
break;
default:
text = i18n.t("confirm");
}
return {
time: `${startTime} - ${endTime}`,
text
};
};
export const buildPeriodsFromSlots = slots => {
const sortedSlots = slots.sort((a, b) => {
if (a.start < b.start) {
return -1;
}
if (a.start > b.start) {
return 1;
}
return 0;
});
const periods = sortedSlots.reduce((acc, curr, i) => {
const prev = slots[i - 1];
if (!prev) {
// This is the last slot, so just add it to the array
return [...acc, curr];
}
if (curr.start === prev.end) {
// This slot abuts the one that came before, so
// let's remove the previous one and combine it with
// the current one.
const lastItem = acc.pop(); // Note: we're relying on pop() mutating acc here
const combo = {
start: lastItem.start,
end: curr.end
};
return [...acc, combo];
}
// The slots don't abut, so just add this one
return [...acc, curr];
}, []);
return periods;
};
export const addSlotStarts = (start, interval, count) => {
const times = [];
for (let i = 0; i <= count; i++) {
const time = start.clone().add(i * interval, "minutes");
times.push(time.format("YYYY-MM-DDTHH:mm[:00Z]"));
}
return times;
};
// Given a start and end slot-time, caluculate the start-times
// of all slots that fall between those times
export const tweenSlots = ({ start, end, tzid, interval }) => {
const first = start < end ? start : end;
const last = start > end ? start : end;
const startTime = moment.utc(first, "YYYY-MM-DDTHH:mm:00Z");
const endTime = moment.utc(last, "YYYY-MM-DDTHH:mm:00Z");
const diff = endTime.diff(startTime, "minutes");
const firstDate = startTime.clone().tz(tzid);
const lastDate = endTime.clone().tz(tzid);
if (firstDate.format("YYYY-MM-DD") !== lastDate.format("YYYY-MM-DD")) {
// The selection spans mulitple days
const localFirstTime = startTime.clone().tz(tzid).format("HH:mm");
const localLastTime = endTime.clone().tz(tzid).format("HH:mm");
const sortedLocalFirstTime =
localFirstTime < localLastTime ? localFirstTime : localLastTime;
const sortedLocalLastTime =
localFirstTime > localLastTime ? localFirstTime : localLastTime;
const timeDiff = moment(sortedLocalLastTime, "HH:mm").diff(
moment(sortedLocalFirstTime, "HH:mm"),
"minutes"
);
const dayTweenCount = timeDiff / interval;
const dayDiff = lastDate
.clone()
.endOf("day")
.diff(firstDate.clone().startOf("day"), "hours");
const dayCount = Math.floor(dayDiff / 24);
const dates = [firstDate.format("YYYY-MM-DD")];
for (let i = 1; i < dayCount; i++) {
const tweenDay = firstDate.clone().add(i, "days");
dates.push(tweenDay.format("YYYY-MM-DD"));
}
dates.push(lastDate.format("YYYY-MM-DD"));
const daysTweens = dates.map(day => {
const dayStart = moment
.tz(`${day} ${sortedLocalFirstTime}`, "YYYY-MM-DD HH:mm", tzid)
.utc();
const dayTweens = addSlotStarts(dayStart, interval, dayTweenCount);
return dayTweens;
});
const flattenedTweens = [].concat.apply([], daysTweens);
return flattenedTweens;
}
const tweenCount = diff / interval;
const tweens = addSlotStarts(startTime, interval, tweenCount);
return tweens;
};
export const calculateTimeString = (slot, i18n, tzid) => {
const startTimeString = i18n
.f(moment(slot.start, "YYYY-MM-DDTHH:mm:00Z").tz(tzid), "LT")
.replace(" ", "");
const endTimeString = i18n
.f(moment(slot.end, "YYYY-MM-DDTHH:mm:00Z").tz(tzid), "LT")
.replace(" ", "");
const timeString = `${startTimeString} - ${endTimeString}`;
return timeString;
};
export const getSelectedSlots = slots => {
const flattenedSlots = [].concat.apply(
[],
slots.map(day => day.slots)
);
const selectedSlots = flattenedSlots.filter(slot => slot.selected);
return selectedSlots;
};
export const checkSelectedState = (newSlots, oldSlots) => {
// Slots are grouped by day.
const mergedSlots = newSlots.map(day => {
const match = oldSlots.find(d => d.day === day.day);
if (match && day.slots.length === match.slots.length) {
// If there is a match (and they have the same number of slots)
// then it's safe to assume that the slots match 1:1 and that
// `day.slots[n]` is the same slot as `match.slots[n]`
const updated = day.slots.map((slot, i) => {
const old = match.slots[i];
if (old.selected) {
// This slot should be "selected"
return {
...slot,
selected: true
};
}
// The slot is not "selected", and can be returned unchanged.
return slot;
});
return { ...day, slots: updated };
}
return day;
});
return mergedSlots;
};
{
"availability_viewer": {
"available": "Available",
"booked": "Booked",
"confirm": "Confirm",

@@ -5,0 +6,0 @@ "end": "End",

@@ -229,2 +229,3 @@ import moment from "moment-timezone";

end_time: "02:00",
interval: 60,
week_start_day: "monday"

@@ -240,8 +241,9 @@ },

boundsControl: true,
end_time: "02:00",
interval: 60,
logs: "warn",
mode: "confirm",
logs: "warn",
slot_selection: "available",
start_time: "01:00",
end_time: "02:00",
startDay: "monday"
startDay: "monday",
start_time: "01:00"
},

@@ -248,0 +250,0 @@ customtzid: true,

@@ -709,2 +709,3 @@ import * as utils from "../src/js/helpers/utils.AvailabilityViewer";

// checkWeekdaysSlotAvailability
it("checks visibility status", () => {

@@ -824,2 +825,202 @@ const emptySlots = [

});
// buildPeriodsFromSlots
it("combines abutting slots into periods", () => {
const slots = [
{
start: "2020-06-16T08:00:00Z",
end: "2020-06-16T08:30:00Z"
},
{
start: "2020-06-16T08:30:00Z",
end: "2020-06-16T09:00:00Z"
},
{
start: "2020-06-16T10:00:00Z",
end: "2020-06-16T10:30:00Z"
},
{
start: "2020-06-16T10:30:00Z",
end: "2020-06-16T11:00:00Z"
},
{
start: "2020-06-16T11:00:00Z",
end: "2020-06-16T11:30:00Z"
}
];
const expectedPeriods = [
{
start: "2020-06-16T08:00:00Z",
end: "2020-06-16T09:00:00Z"
},
{
start: "2020-06-16T10:00:00Z",
end: "2020-06-16T11:30:00Z"
}
];
const periods = utils.buildPeriodsFromSlots(slots);
expect(periods.length).toEqual(2);
expect(periods[0]).toEqual(expectedPeriods[0]);
});
it("combines unsorted abutting slots into periods", () => {
const slots = [
{
start: "2020-06-16T08:30:00Z",
end: "2020-06-16T09:00:00Z"
},
{
start: "2020-06-16T08:00:00Z",
end: "2020-06-16T08:30:00Z"
},
{
start: "2020-06-16T10:30:00Z",
end: "2020-06-16T11:00:00Z"
},
{
start: "2020-06-17T10:30:00Z",
end: "2020-06-17T11:00:00Z"
},
{
start: "2020-06-16T11:00:00Z",
end: "2020-06-16T11:30:00Z"
},
{
start: "2020-06-16T10:00:00Z",
end: "2020-06-16T10:30:00Z"
}
];
const expectedPeriods = [
{
start: "2020-06-16T08:00:00Z",
end: "2020-06-16T09:00:00Z"
},
{
start: "2020-06-16T10:00:00Z",
end: "2020-06-16T11:30:00Z"
},
{
start: "2020-06-17T10:30:00Z",
end: "2020-06-17T11:00:00Z"
}
];
const periods = utils.buildPeriodsFromSlots(slots);
expect(periods.length).toEqual(3);
expect(periods[0]).toEqual(expectedPeriods[0]);
expect(periods[1]).toEqual(expectedPeriods[1]);
expect(periods[2]).toEqual(expectedPeriods[2]);
});
// tweenSlots
it("calculates tweens for slot-starts", () => {
const options = {
start: "2020-06-16T08:00:00Z",
end: "2020-06-16T11:30:00Z",
tzid: "Europe/London",
interval: 30
};
const expected = [
"2020-06-16T08:00:00Z",
"2020-06-16T08:30:00Z",
"2020-06-16T09:00:00Z",
"2020-06-16T09:30:00Z",
"2020-06-16T10:00:00Z",
"2020-06-16T10:30:00Z",
"2020-06-16T11:00:00Z",
"2020-06-16T11:30:00Z"
];
const result = utils.tweenSlots(options);
expect(result).toEqual(expected);
expect(Array.isArray(result)).toBe(true);
expect(result.length).toBe(expected.length);
});
it("calculates tweens for cross-day slot-starts", () => {
const options = {
start: "2020-06-16T08:00:00Z",
end: "2020-06-17T10:00:00Z",
tzid: "America/Toronto",
interval: 60
};
const expected = [
"2020-06-16T08:00:00Z",
"2020-06-16T09:00:00Z",
"2020-06-16T10:00:00Z",
"2020-06-17T08:00:00Z",
"2020-06-17T09:00:00Z",
"2020-06-17T10:00:00Z"
];
const result = utils.tweenSlots(options);
expect(result).toEqual(expected);
expect(Array.isArray(result)).toBe(true);
expect(result.length).toBe(expected.length);
});
it("calculates tweens for cross-day slot-starts over timezone boundaries", () => {
const options = {
// In America/Toronto, these times cross midnight
// so there should be blocks for FOUR days (not
// just the three that 23rd - 25th would suggest)
start: "2020-06-23T02:00:00Z",
end: "2020-06-25T21:00:00Z",
tzid: "America/Toronto",
interval: 60
};
const expected = [
// Block 1
"2020-06-22T21:00:00Z",
"2020-06-22T22:00:00Z",
"2020-06-22T23:00:00Z",
"2020-06-23T00:00:00Z",
"2020-06-23T01:00:00Z",
"2020-06-23T02:00:00Z",
// Block 2
"2020-06-23T21:00:00Z",
"2020-06-23T22:00:00Z",
"2020-06-23T23:00:00Z",
"2020-06-24T00:00:00Z",
"2020-06-24T01:00:00Z",
"2020-06-24T02:00:00Z",
// Block 3
"2020-06-24T21:00:00Z",
"2020-06-24T22:00:00Z",
"2020-06-24T23:00:00Z",
"2020-06-25T00:00:00Z",
"2020-06-25T01:00:00Z",
"2020-06-25T02:00:00Z",
// Block 4
"2020-06-25T21:00:00Z",
"2020-06-25T22:00:00Z",
"2020-06-25T23:00:00Z",
"2020-06-26T00:00:00Z",
"2020-06-26T01:00:00Z",
"2020-06-26T02:00:00Z"
];
const result = utils.tweenSlots(options);
console.log("expected", expected);
console.log("result", result);
expect(result).toEqual(expected);
expect(Array.isArray(result)).toBe(true);
expect(result.length).toBe(expected.length);
});
// addSlotStarts
it("adds N slot start times", () => {
const options = [
moment("2020-06-16T08:00:00Z", "YYYY-MM-DDTHH:mm:00Z"),
30,
4
];
const expected = [
"2020-06-16T08:00:00Z",
"2020-06-16T08:30:00Z",
"2020-06-16T09:00:00Z",
"2020-06-16T09:30:00Z",
"2020-06-16T10:00:00Z"
];
const result = utils.addSlotStarts(...options);
expect(result).toEqual(expected);
});
});

Sorry, the diff of this file is too big to display

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc