mirror of
https://github.com/mvvasilev/findtheti.me.git
synced 2025-04-19 13:39:52 +03:00
Optimize AvailabilityPicker performance
This commit is contained in:
parent
a2bb7fab1c
commit
988431d004
4 changed files with 196 additions and 142 deletions
|
@ -13,7 +13,10 @@
|
||||||
"@emotion/react": "^11.11.3",
|
"@emotion/react": "^11.11.3",
|
||||||
"@emotion/styled": "^11.11.0",
|
"@emotion/styled": "^11.11.0",
|
||||||
"@mui/material": "^5.15.3",
|
"@mui/material": "^5.15.3",
|
||||||
|
"@mui/system": "^5.15.3",
|
||||||
"@mui/x-date-pickers": "^6.18.7",
|
"@mui/x-date-pickers": "^6.18.7",
|
||||||
|
"classname": "^0.0.0",
|
||||||
|
"classnames": "^2.5.1",
|
||||||
"dayjs": "^1.11.10",
|
"dayjs": "^1.11.10",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
|
|
|
@ -1,12 +1,13 @@
|
||||||
import { Box, Card, Divider, Stack, Theme, Typography, useTheme } from "@mui/material";
|
import { Box, Card, Divider, Stack, Typography } from "@mui/material";
|
||||||
import dayjs, { Dayjs } from "dayjs";
|
import dayjs, { Dayjs } from "dayjs";
|
||||||
import { MouseEvent, ReactNode, useEffect, useState } from "react";
|
import { MouseEvent, useEffect, useState } from "react";
|
||||||
import * as utc from 'dayjs/plugin/utc';
|
import * as utc from 'dayjs/plugin/utc';
|
||||||
import * as timezone from 'dayjs/plugin/timezone';
|
import * as timezone from 'dayjs/plugin/timezone';
|
||||||
import * as localizedFormat from 'dayjs/plugin/localizedFormat';
|
import * as localizedFormat from 'dayjs/plugin/localizedFormat';
|
||||||
import utils from "../utils";
|
import utils from "../utils";
|
||||||
import { EventTypes } from "../types/Event";
|
import { EventTypes } from "../types/Event";
|
||||||
import "./css/AvailabilityPicker.css";
|
import "./css/AvailabilityPicker.css";
|
||||||
|
import classNames from 'classnames';
|
||||||
// import { alpha } from '@material-ui/core/styles/colorManipulator';
|
// import { alpha } from '@material-ui/core/styles/colorManipulator';
|
||||||
|
|
||||||
dayjs.extend(utc)
|
dayjs.extend(utc)
|
||||||
|
@ -26,21 +27,125 @@ type AvailabilityDay = {
|
||||||
type GhostPreviewProps = {
|
type GhostPreviewProps = {
|
||||||
top: number,
|
top: number,
|
||||||
left: number,
|
left: number,
|
||||||
width: String,
|
width: number,
|
||||||
height: String
|
height: number
|
||||||
}
|
}
|
||||||
|
|
||||||
const HALFHOUR_DISPLAY_HEIGHT: number = 15;
|
const HALFHOUR_DISPLAY_HEIGHT: number = 15;
|
||||||
|
|
||||||
const DAY_DISPLAY_WIDTH: String = "150px";
|
const Hour = (props: {
|
||||||
|
dateTime: Dayjs,
|
||||||
|
isFullHourSelected: boolean,
|
||||||
|
isHalfHourSelected: boolean,
|
||||||
|
onMouseEnterHalfhour: (e: MouseEvent<HTMLDivElement, globalThis.MouseEvent>, time: Dayjs) => void,
|
||||||
|
onMouseClickOnHalfhour: (time: Dayjs) => void
|
||||||
|
}) => {
|
||||||
|
let isEvenHour = props.dateTime.hour() % 2 == 0;
|
||||||
|
|
||||||
export default function AvailabilityPicker(props: {
|
return (
|
||||||
|
<Box
|
||||||
|
className={classNames({ "hour-light": isEvenHour, "hour-dark": !isEvenHour })}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
className={classNames("full-hour", { "selected-availability": props.isFullHourSelected })}
|
||||||
|
height={HALFHOUR_DISPLAY_HEIGHT}
|
||||||
|
onMouseEnter={(e) => props.onMouseEnterHalfhour(e, props.dateTime)}
|
||||||
|
onClick={(_) => props.onMouseClickOnHalfhour(props.dateTime)}
|
||||||
|
>
|
||||||
|
<Typography className={"noselect time-text"}>
|
||||||
|
{ utils.formatTimeFromHourOfDay(props.dateTime.hour(), 0) }
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
<Box
|
||||||
|
className={classNames("half-hour", { "selected-availability": props.isHalfHourSelected })}
|
||||||
|
height={HALFHOUR_DISPLAY_HEIGHT}
|
||||||
|
onMouseEnter={(e) => props.onMouseEnterHalfhour(e, props.dateTime.add(30, "minutes"))}
|
||||||
|
onClick={(_) => props.onMouseClickOnHalfhour(props.dateTime.add(30, "minutes"))}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const isSelectedAvailability = (day: AvailabilityDay, time: Dayjs): boolean => {
|
||||||
|
return day.availableTimes.some(t => t.fromTime.unix() <= time.unix() && time.unix() <= t.toTime.unix());
|
||||||
|
}
|
||||||
|
|
||||||
|
const Day = (props: {
|
||||||
|
day: AvailabilityDay,
|
||||||
|
eventType: String,
|
||||||
|
onMouseEnterHalfhour: (e: MouseEvent<HTMLDivElement, globalThis.MouseEvent>, time: dayjs.Dayjs) => void,
|
||||||
|
onMouseClickHalfhour: (day: AvailabilityDay, time: dayjs.Dayjs) => void
|
||||||
|
}) => {
|
||||||
|
|
||||||
|
const generateHours = (): JSX.Element[] => {
|
||||||
|
let hours: JSX.Element[] = [];
|
||||||
|
|
||||||
|
for (var i = 0; i < 24; i++) {
|
||||||
|
let time = props.day.forDate.set("hour", i);
|
||||||
|
hours.push(
|
||||||
|
<Hour
|
||||||
|
key={time.unix()}
|
||||||
|
dateTime={time}
|
||||||
|
isFullHourSelected={isSelectedAvailability(props.day, time)}
|
||||||
|
isHalfHourSelected={isSelectedAvailability(props.day, time.set("minutes", 30))}
|
||||||
|
onMouseEnterHalfhour={(e: MouseEvent<HTMLDivElement, globalThis.MouseEvent>, time: dayjs.Dayjs): void => {
|
||||||
|
props.onMouseEnterHalfhour(e, time);
|
||||||
|
}}
|
||||||
|
onMouseClickOnHalfhour={(time: dayjs.Dayjs): void => {
|
||||||
|
props.onMouseClickHalfhour(props.day, time);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return hours;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card
|
||||||
|
|
||||||
|
key={props.day.forDate.format()}
|
||||||
|
className={"day-card"}
|
||||||
|
variant="outlined"
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
sx={{ width: "100%" }}
|
||||||
|
padding={1}
|
||||||
|
>
|
||||||
|
{
|
||||||
|
(props.eventType === EventTypes.WEEK) &&
|
||||||
|
<Typography>
|
||||||
|
{ props.day.forDate.format("dddd") }
|
||||||
|
</Typography>
|
||||||
|
}
|
||||||
|
{
|
||||||
|
(props.eventType === EventTypes.DAY) &&
|
||||||
|
<Typography>
|
||||||
|
Any Day
|
||||||
|
</Typography>
|
||||||
|
}
|
||||||
|
{
|
||||||
|
(props.eventType === EventTypes.DATE_RANGE || props.eventType === EventTypes.SPECIFIC_DATE) &&
|
||||||
|
<Typography>
|
||||||
|
{ props.day.forDate.format("LL") }
|
||||||
|
</Typography>
|
||||||
|
}
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Divider></Divider>
|
||||||
|
|
||||||
|
{generateHours()}
|
||||||
|
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const AvailabilityPicker = (props: {
|
||||||
fromDate: Dayjs,
|
fromDate: Dayjs,
|
||||||
toDate: Dayjs,
|
toDate: Dayjs,
|
||||||
eventType: String,
|
eventType: String,
|
||||||
availabilityDurationInMinutes: number
|
availabilityDurationInMinutes: number
|
||||||
}) {
|
}) => {
|
||||||
const theme: Theme = useTheme();
|
|
||||||
|
|
||||||
const [days, setDays] = useState<AvailabilityDay[]>([]);
|
const [days, setDays] = useState<AvailabilityDay[]>([]);
|
||||||
const [ghostPreviewProps, setGhostPreviewProps] = useState<GhostPreviewProps | null>();
|
const [ghostPreviewProps, setGhostPreviewProps] = useState<GhostPreviewProps | null>();
|
||||||
|
@ -71,11 +176,11 @@ export default function AvailabilityPicker(props: {
|
||||||
}
|
}
|
||||||
}, [props]);
|
}, [props]);
|
||||||
|
|
||||||
function createAvailabilitiesBasedOnUnspecifiedInitialDate(numberOfDays: number, tz: string) {
|
const createAvailabilitiesBasedOnUnspecifiedInitialDate = (numberOfDays: number, tz: string) => {
|
||||||
createAvailabilitiesBasedOnInitialDate(dayjs.tz("1970-01-05 00:00:00", tz), numberOfDays);
|
createAvailabilitiesBasedOnInitialDate(dayjs.tz("1970-01-05 00:00:00", tz), numberOfDays);
|
||||||
}
|
}
|
||||||
|
|
||||||
function createAvailabilitiesBasedOnInitialDate(date: Dayjs, numberOfDays: number) {
|
const createAvailabilitiesBasedOnInitialDate = (date: Dayjs, numberOfDays: number) => {
|
||||||
let availabilities: AvailabilityDay[] = [];
|
let availabilities: AvailabilityDay[] = [];
|
||||||
|
|
||||||
for (var i: number = 0; i < numberOfDays; i++) {
|
for (var i: number = 0; i < numberOfDays; i++) {
|
||||||
|
@ -90,7 +195,7 @@ export default function AvailabilityPicker(props: {
|
||||||
setDays(availabilities);
|
setDays(availabilities);
|
||||||
}
|
}
|
||||||
|
|
||||||
function displayGhostPeriod(e: React.MouseEvent<HTMLDivElement, MouseEvent>, time: Dayjs) {
|
const displayGhostPeriod = (e: MouseEvent<HTMLDivElement, globalThis.MouseEvent>, time: Dayjs) => {
|
||||||
let timeInMinutes = (time.hour() * 60) + time.minute();
|
let timeInMinutes = (time.hour() * 60) + time.minute();
|
||||||
let timeLeftInDayLessThanDuration = (timeInMinutes + props.availabilityDurationInMinutes) > 24 * 60;
|
let timeLeftInDayLessThanDuration = (timeInMinutes + props.availabilityDurationInMinutes) > 24 * 60;
|
||||||
|
|
||||||
|
@ -101,17 +206,20 @@ export default function AvailabilityPicker(props: {
|
||||||
let scrollTop = document.getElementById('availability-picker')?.scrollTop ?? 0;
|
let scrollTop = document.getElementById('availability-picker')?.scrollTop ?? 0;
|
||||||
let scrollLeft = document.getElementById('availability-picker')?.scrollLeft ?? 0;
|
let scrollLeft = document.getElementById('availability-picker')?.scrollLeft ?? 0;
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
const element = e.target.getBoundingClientRect();
|
const element = e.target.getBoundingClientRect();
|
||||||
|
|
||||||
setGhostPreviewProps({
|
setGhostPreviewProps({
|
||||||
|
// @ts-ignore
|
||||||
top: e.target?.offsetTop - scrollTop,
|
top: e.target?.offsetTop - scrollTop,
|
||||||
|
// @ts-ignore
|
||||||
left: e.target?.offsetLeft - scrollLeft,
|
left: e.target?.offsetLeft - scrollLeft,
|
||||||
width: element.width,
|
width: element.width,
|
||||||
height: `${(props.availabilityDurationInMinutes/60) * 2 * HALFHOUR_DISPLAY_HEIGHT}px`
|
height: (props.availabilityDurationInMinutes/60) * 2 * HALFHOUR_DISPLAY_HEIGHT
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function createAvailability(day: AvailabilityDay, time: Dayjs) {
|
const createAvailability = (day: AvailabilityDay, time: Dayjs) => {
|
||||||
let fromTime = time;
|
let fromTime = time;
|
||||||
let toTime = time.add(props.availabilityDurationInMinutes, "minutes");
|
let toTime = time.add(props.availabilityDurationInMinutes, "minutes");
|
||||||
|
|
||||||
|
@ -175,151 +283,47 @@ export default function AvailabilityPicker(props: {
|
||||||
setDays([...days])
|
setDays([...days])
|
||||||
}
|
}
|
||||||
|
|
||||||
function isSelectedAvailability(day: AvailabilityDay, time: Dayjs): boolean {
|
|
||||||
return day.availableTimes.some(t => (t.fromTime.isBefore(time) || t.fromTime.isSame(time)) && (t.toTime.isAfter(time) || t.toTime.isSame(time)));
|
|
||||||
}
|
|
||||||
|
|
||||||
function generateDay(day: AvailabilityDay) {
|
|
||||||
|
|
||||||
const HOUR_TEXT_COLOR: String = "#ddd";
|
|
||||||
|
|
||||||
let hours = [];
|
|
||||||
|
|
||||||
for (var i = 0; i < 24; i++) {
|
|
||||||
let time = day.forDate.set("hour", i).set("minute", 0).set("second", 0);
|
|
||||||
|
|
||||||
hours.push(
|
|
||||||
<Box
|
|
||||||
key={`${i}`}
|
|
||||||
className={(i % 2 == 0) ? "hour-light" : "hour-dark"}
|
|
||||||
>
|
|
||||||
<Box
|
|
||||||
className={[
|
|
||||||
"full-hour",
|
|
||||||
isSelectedAvailability(day, time) && "selected-availability"
|
|
||||||
]}
|
|
||||||
height={HALFHOUR_DISPLAY_HEIGHT}
|
|
||||||
onMouseEnter={(e: MouseEvent<HTMLDivElement, MouseEvent>) => displayGhostPeriod(e, time)}
|
|
||||||
onClick={(e) => createAvailability(day, time)}
|
|
||||||
>
|
|
||||||
<Typography
|
|
||||||
className={"noselect"}
|
|
||||||
textAlign={"left"}
|
|
||||||
fontSize={"0.65em"}
|
|
||||||
color={HOUR_TEXT_COLOR}
|
|
||||||
>
|
|
||||||
{ utils.formatTimeFromHourOfDay(i, 0) }
|
|
||||||
</Typography>
|
|
||||||
</Box>
|
|
||||||
<Box
|
|
||||||
className={[
|
|
||||||
"half-hour",
|
|
||||||
isSelectedAvailability(day, time) && "selected-availability"
|
|
||||||
]}
|
|
||||||
height={HALFHOUR_DISPLAY_HEIGHT}
|
|
||||||
onMouseEnter={(e: MouseEvent<HTMLDivElement, MouseEvent>) => displayGhostPeriod(e, time.add(30, "minutes"))}
|
|
||||||
onClick={(e) => createAvailability(day, time.add(30, "minutes"))}
|
|
||||||
>
|
|
||||||
</Box>
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Stack
|
|
||||||
key={day.forDate.format()}
|
|
||||||
direction="column"
|
|
||||||
sx={{
|
|
||||||
minWidth: DAY_DISPLAY_WIDTH,
|
|
||||||
width: DAY_DISPLAY_WIDTH
|
|
||||||
}}
|
|
||||||
overflow={"visible"}
|
|
||||||
>
|
|
||||||
<Card
|
|
||||||
sx={{
|
|
||||||
width: "100%",
|
|
||||||
height: "fit-content",
|
|
||||||
overflow: "visible"
|
|
||||||
}}
|
|
||||||
variant="outlined"
|
|
||||||
>
|
|
||||||
<Box
|
|
||||||
sx={{ width: "100%" }}
|
|
||||||
padding={1}
|
|
||||||
>
|
|
||||||
{
|
|
||||||
(props.eventType === EventTypes.WEEK) &&
|
|
||||||
<Typography>
|
|
||||||
{ day.forDate.format("dddd") }
|
|
||||||
</Typography>
|
|
||||||
}
|
|
||||||
{
|
|
||||||
(props.eventType === EventTypes.DAY) &&
|
|
||||||
<Typography>
|
|
||||||
Any Day
|
|
||||||
</Typography>
|
|
||||||
}
|
|
||||||
{
|
|
||||||
(props.eventType === EventTypes.DATE_RANGE || props.eventType === EventTypes.SPECIFIC_DATE) &&
|
|
||||||
<Typography>
|
|
||||||
{ day.forDate.format("LL") }
|
|
||||||
</Typography>
|
|
||||||
}
|
|
||||||
</Box>
|
|
||||||
<Divider></Divider>
|
|
||||||
|
|
||||||
{hours}
|
|
||||||
</Card>
|
|
||||||
</Stack>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
className={"availability-parent-box"}
|
||||||
position: "relative",
|
|
||||||
width: "100%",
|
|
||||||
height: "auto",
|
|
||||||
overflow: "hidden"
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<Stack
|
<Stack
|
||||||
id="availability-picker"
|
id="availability-picker"
|
||||||
direction="row"
|
direction="row"
|
||||||
spacing={1}
|
spacing={1}
|
||||||
justifyContent={"safe center"}
|
justifyContent={"safe center"}
|
||||||
sx={{
|
className={"availability-parent-stack"}
|
||||||
width: "100%",
|
onScroll={(_) => setGhostPreviewProps(null)}
|
||||||
height: "500px",
|
|
||||||
overflowY: "scroll",
|
|
||||||
overflowX: "scroll"
|
|
||||||
}}
|
|
||||||
onScroll={(e) => setGhostPreviewProps(null)}
|
|
||||||
>
|
>
|
||||||
{
|
{
|
||||||
days.map(a => generateDay(a))
|
days.map(day =>
|
||||||
|
<Day
|
||||||
|
key={day.forDate.unix()}
|
||||||
|
day={day}
|
||||||
|
eventType={props.eventType}
|
||||||
|
onMouseEnterHalfhour={(e: MouseEvent<HTMLDivElement, globalThis.MouseEvent>, time: dayjs.Dayjs) => {
|
||||||
|
displayGhostPeriod(e, time);
|
||||||
|
}}
|
||||||
|
onMouseClickHalfhour={(day: AvailabilityDay, time: dayjs.Dayjs) => {
|
||||||
|
createAvailability(day, time);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
</Stack>
|
</Stack>
|
||||||
{
|
{
|
||||||
(ghostPreviewProps !== null) &&
|
(ghostPreviewProps !== null) &&
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
className={"ghost-box"}
|
||||||
position: "absolute",
|
top={ghostPreviewProps?.top}
|
||||||
top: ghostPreviewProps?.top,
|
left={ghostPreviewProps?.left}
|
||||||
left: ghostPreviewProps?.left,
|
width={ghostPreviewProps?.width}
|
||||||
width: ghostPreviewProps?.width,
|
height={ghostPreviewProps?.height}
|
||||||
height: ghostPreviewProps?.height,
|
|
||||||
bgcolor: "rgba(0, 255, 0, 0.1)",
|
|
||||||
border: 1,
|
|
||||||
borderColor: "#272",
|
|
||||||
borderRadius: 1,
|
|
||||||
m: 0,
|
|
||||||
p: 0,
|
|
||||||
pointerEvents: "none"
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
</Box>
|
</Box>
|
||||||
}
|
}
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default AvailabilityPicker;
|
||||||
|
|
|
@ -44,3 +44,40 @@ div.full-hour:hover, div.half-hour:hover {
|
||||||
div.full-hour:active, div.half-hour:active {
|
div.full-hour:active, div.half-hour:active {
|
||||||
background-color: var(--active-color);
|
background-color: var(--active-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
div.ghost-box {
|
||||||
|
position: absolute;
|
||||||
|
background-color: rgba(0, 255, 0, 0.1);
|
||||||
|
border: solid 1px;
|
||||||
|
border-color: #227722;
|
||||||
|
border-radius: 1;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
p.time-text {
|
||||||
|
text-align: left;
|
||||||
|
font-size: 0.65em;
|
||||||
|
color: #ddd;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.day-card {
|
||||||
|
width: 120px;
|
||||||
|
min-width: 120px;
|
||||||
|
height: fit-content;
|
||||||
|
overflow: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.availability-parent-box {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.availability-parent-stack {
|
||||||
|
width: 100%;
|
||||||
|
height: 500px;
|
||||||
|
overflow: scroll;
|
||||||
|
}
|
|
@ -616,7 +616,7 @@
|
||||||
|
|
||||||
"@mui/system@^5.15.3":
|
"@mui/system@^5.15.3":
|
||||||
version "5.15.3"
|
version "5.15.3"
|
||||||
resolved "https://registry.npmjs.org/@mui/system/-/system-5.15.3.tgz"
|
resolved "https://registry.yarnpkg.com/@mui/system/-/system-5.15.3.tgz#062d0d6b5259c3dc0e1d4026b85ffcc3acf8637b"
|
||||||
integrity sha512-ewVU4eRgo4VfNMGpO61cKlfWmH7l9s6rA8EknRzuMX3DbSLfmtW2WJJg6qPwragvpPIir0Pp/AdWVSDhyNy5Tw==
|
integrity sha512-ewVU4eRgo4VfNMGpO61cKlfWmH7l9s6rA8EknRzuMX3DbSLfmtW2WJJg6qPwragvpPIir0Pp/AdWVSDhyNy5Tw==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@babel/runtime" "^7.23.6"
|
"@babel/runtime" "^7.23.6"
|
||||||
|
@ -1069,6 +1069,16 @@ chalk@^4.0.0:
|
||||||
ansi-styles "^4.1.0"
|
ansi-styles "^4.1.0"
|
||||||
supports-color "^7.1.0"
|
supports-color "^7.1.0"
|
||||||
|
|
||||||
|
classname@^0.0.0:
|
||||||
|
version "0.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/classname/-/classname-0.0.0.tgz#43d171b484e354c7a293a5b78004df6852db20d6"
|
||||||
|
integrity sha512-kkhsspEJdUW+VhuvNzb2sQf0KbafDPfd36dB1qf03Uu42dWZwMQzaQuyNkaRr5ir0ZiAN0+TlH/EOOfwb/aaXg==
|
||||||
|
|
||||||
|
classnames@^2.5.1:
|
||||||
|
version "2.5.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.5.1.tgz#ba774c614be0f016da105c858e7159eae8e7687b"
|
||||||
|
integrity sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==
|
||||||
|
|
||||||
clsx@^2.0.0:
|
clsx@^2.0.0:
|
||||||
version "2.1.0"
|
version "2.1.0"
|
||||||
resolved "https://registry.npmjs.org/clsx/-/clsx-2.1.0.tgz"
|
resolved "https://registry.npmjs.org/clsx/-/clsx-2.1.0.tgz"
|
||||||
|
|
Loading…
Add table
Reference in a new issue