Skip to content

XS-1082 Dashboard >> Header Redesign #370

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 30 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
03220d6
Added CalendarListIcon
KarthickXola May 14, 2025
aaadaf5
Added CalendarListIcon
KarthickXola May 14, 2025
d45e1d0
Added CalendarListIcon
KarthickXola May 14, 2025
8e381a4
WIP - Month picker
KarthickXola May 15, 2025
3d72ad9
WIP - Month picker
KarthickXola May 15, 2025
3c8d74d
WIP - Month picker
KarthickXola May 15, 2025
f364c89
WIP - Month picker
KarthickXola May 15, 2025
5f76105
WIP - Month picker
KarthickXola May 15, 2025
c349cd9
WIP - Month picker popover
KarthickXola May 16, 2025
b531fbf
WIP - Month picker popover
KarthickXola May 16, 2025
788be1f
WIP - Month picker popover
KarthickXola May 16, 2025
74581b9
WIP - Month picker popover
KarthickXola May 16, 2025
e4ff567
WIP - Month picker popover
KarthickXola May 16, 2025
764fc78
WIP - Month picker popover
KarthickXola May 16, 2025
fc80af0
WIP - Month picker popover
KarthickXola May 16, 2025
f8b21f6
WIP - Month picker popover
KarthickXola May 16, 2025
2d1424d
WIP - Month picker popover
KarthickXola May 16, 2025
407a5d9
refactor
KarthickXola May 16, 2025
73ee57b
added selected days param
KarthickXola May 19, 2025
647bab3
revert added selected days param
KarthickXola May 19, 2025
bb7304f
added selected days param
KarthickXola May 20, 2025
4ed99d7
added selected days param
KarthickXola May 20, 2025
adc95e8
added selected days param
KarthickXola May 20, 2025
98669cc
added padding
KarthickXola May 22, 2025
2e7da56
added padding
KarthickXola May 22, 2025
0b15bfd
used popover for month context
KarthickXola May 22, 2025
706bf77
used popover for month context
KarthickXola May 22, 2025
d76eb13
used popover for month context
KarthickXola May 22, 2025
584453c
used popover for month context
KarthickXola May 22, 2025
f09ca1d
refactor
KarthickXola May 23, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ export {
CakeIcon,
CalendarDayIcon,
CalendarIcon,
CalendarListIcon,
CalendarMonthIcon,
CalendarWeekIcon,
CapacityIcon,
Expand Down Expand Up @@ -177,6 +178,7 @@ export {
MoneyBackIcon,
MoneyIcon,
MountainIcon,
MonthPicker,
MouseIcon,
Number,
numberFormat,
Expand Down
51 changes: 37 additions & 14 deletions src/components/DatePicker/DatePicker.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import { Day } from "./Day";
import { LocalizedDayPicker } from "./LocalizedDayPicker";
import { MonthYearSelector } from "./MonthYearSelector";
import { MonthSelector } from "./MonthSelector";
import { NavbarElement } from "./NavbarElement";
import RangeDatePicker from "./RangeDatePicker";
import { RelativeDateRange } from "./RelativeDateRange";
Expand All @@ -25,305 +26,327 @@
/**
* Figma Design link: https://www.figma.com/file/tL2vrxuBIzujkDfYvVjUhs/%F0%9F%9B%A0-Xola-DS-Desktop-Master-%F0%9F%9B%A0?node-id=2689%3A101580
*/
export const DatePicker = ({
variant = variants.single,
value,
getDayContent,
disabledDays = [],
selectedDays,
loadingDays = [],
shouldShowYearPicker = false,
shouldShowMonthSelector = false,
onChange,
onMonthChange,
onSubmitDateRange,
modifiers = {},
ranges,
shouldShowRelativeRanges = false,
isFutureDatesAllowed = false,
components = {},
getTooltip,
upcomingDates,
locale,
timezoneName = null, // seller timezone (e.g. "America/Los_Angeles") to return correct today date
...rest
}) => {
const { locale: contextLocale } = useContext(Context);
const initialValue = value ? (variant === variants.single ? value : value.from) : null;
const [currentMonth, setCurrentMonth] = useState(initialValue ?? now(null, timezoneName).toDate());
const [startMonth, setStartMonth] = useState(() => {
if (!value || !value.from) {
return new Date();
}

return value.from;
});
const [endMonth, setEndMonth] = useState(() => {
if (!value || !value.to || !value.from) {
return now(null, timezoneName).add(1, "month").toDate();
}

return isSame(now(value.to, timezoneName), now(value.from, timezoneName), "month")
? now(value.from, timezoneName).add(1, "month").toDate()
: value.to;
});
const [rangeName, setRangeName] = useState("");
const isRangeVariant = variant === variants.range;
const isValidValue = value && value.from && value.to;
const isSelectedDaysHasValidRange = selectedDays && selectedDays.from && selectedDays.to;

useEffect(() => {
if (timezoneName && !isValidTimeZoneName(timezoneName)) {
console.log(`${timezoneName} is not a valid timezone. Using default timezone now`);
dayjs.tz.setDefault();
}
}, [timezoneName]);

const handleTodayClick = (day, options, event) => {
if (isRangeVariant) {
return;
}

const today = timezoneName ? toDate(now(day, timezoneName)) : new Date();

if (options.disabled || isDisabled(today)) {
setCurrentMonth(today);
onMonthChange?.(today);
} else {
onChange(today, options, event);
}
};

const isDisabled = (date) => {
if (isArray(disabledDays)) {
return disabledDays.some((_date) => isSame(now(_date, timezoneName), date, "day"));
}

if (isFunction(disabledDays)) {
return disabledDays(date);
}

return disabledDays(date);
};

const isLoading = (date) => {
if (isArray(loadingDays)) {
return loadingDays.some((_date) => isSame(now(_date, timezoneName), date, "day"));
}

if (isFunction(loadingDays)) {
return loadingDays(date);
}

return loadingDays(date);
};

const handleRelativeRangeChanged = (rangeName, range) => {
setCurrentMonth(range.from);
setStartMonth(range.from);
setEndMonth(range.to);
onChange({ ...range, rangeName }, modifiers, null);
};

const handleMonthChange = (m) => {
setCurrentMonth(m);
onMonthChange?.(m);
};

const handleStartMonthChange = (m) => {
setStartMonth(m);
onMonthChange?.(m);
};

const handleEndMonthChange = (m) => {
setEndMonth(m);
onMonthChange?.(m);
};

const handleDayClick = (day, options, event) => {
if (options.disabled) {
return;
}

if (isSame(now(value?.from, timezoneName), now(day, timezoneName), "month")) {
handleStartMonthChange(day);
}

setRangeName("");
if (isRangeVariant) {
if (isValidValue) {
// This allows us to easily select another date range,
// if both dates are selected.
onChange({ from: toDate(now(day, timezoneName)), to: null }, options, event);
} else if (value && (value.from || value.to) && (value.from || value.to).getTime() === day.getTime()) {
const from = toDate(now(day, timezoneName));
const to = toDate(now(day, timezoneName).endOf("day"), false);

onChange({ from, to }, options, event);
} else if (value.from && DateUtils.isDayBefore(value.from, toDate(now(day, timezoneName)))) {
// this works if the user first clicked on the date that will go to "from", and the second click to "to"
onChange(DateUtils.addDayToRange(toDate(now(day, timezoneName), false), value), options, event);
} else if (
value.from &&
(DateUtils.isDayAfter(value.from, toDate(now(day, timezoneName))) ||
DateUtils.isSameDay(value.from, toDate(now(day, timezoneName))))
) {
// this works if the user first clicked on the date that will go to "to", and the second click to "from"
// also this works when the user has selected one date
onChange(
{
from: toDate(now(day, timezoneName)),
to: toDate(now(value.from).endOf("day"), false),
},
options,
event,
);
} else {
// Fallback when value.from is null
onChange(
DateUtils.addDayToRange(toDate(now(day, timezoneName).endOf("day"), false), value),
options,
event,
);
}
} else {
onChange(toDate(now(day, timezoneName)), options, event);
}
};

// TODO: Should be outside this component because this returns JSX
const CaptionElement = useMemo(() => {
return shouldShowYearPicker && currentMonth
? ({ date }) => (
<MonthYearSelector
date={date}
currentMonth={currentMonth}
locale={locale ?? contextLocale}
onChange={handleMonthChange}
/>
)
return (shouldShowYearPicker || shouldShowMonthSelector) && currentMonth
? ({ date }) =>
shouldShowMonthSelector ? (
<MonthSelector
date={date}
currentMonth={currentMonth}
locale={locale ?? contextLocale}
onChange={handleMonthChange}
/>
) : (
<MonthYearSelector
date={date}
currentMonth={currentMonth}
locale={locale ?? contextLocale}
onChange={handleMonthChange}
/>
)
: undefined;
// Adding `handleMonthChange` causes a lot of re-renders, and closes drop-down.
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [shouldShowYearPicker, currentMonth]);
}, [shouldShowYearPicker, shouldShowMonthSelector, currentMonth]);

// TODO: Should be outside this component because this returns JSX
const renderDay = (date) => {
const tooltipContent = getTooltip?.(date);
const disabled = isDisabled(date);
const loading = isLoading(date);

return tooltipContent ? (
<Tooltip placement="top" content={tooltipContent}>
<Day
disabled={disabled}
isLoading={loading}
selectedDate={value}
date={date}
getContent={getDayContent}
currentMonth={currentMonth}
/>
</Tooltip>
) : (
<Day
disabled={disabled}
isLoading={loading}
selectedDate={value}
date={date}
getContent={getDayContent}
currentMonth={currentMonth}
/>
);
};

const rangeModifier = isRangeVariant && isValidValue ? { start: value.from, end: value.to } : null;
const rangeModifier =
isRangeVariant && isValidValue
? { start: value.from, end: value.to }
: isSelectedDaysHasValidRange
? { start: selectedDays.from, end: selectedDays.to }
: null;

// Comparing `from` and `to` dates hides a weird CSS style when you select the same date twice in a date range.
const useDateRangeStyle = isRangeVariant && isValidValue && value.from?.getTime() !== value.to?.getTime();
const useDateSelectedRangeStyle =
isSelectedDaysHasValidRange && selectedDays.from?.getTime() !== selectedDays.to?.getTime();
// Return the same value if it is already dayjs object or has range variant otherwise format it to dayJs object
const selectedDays = value && (dayjs.isDayjs(value) || isRangeVariant ? value : now(value, timezoneName).toDate());
const selectedDaysValues =
selectedDays ?? (value && (dayjs.isDayjs(value) || isRangeVariant ? value : now(value, timezoneName).toDate()));

return (
<>
<div className="flex">
{upcomingDates ? (
<UpcomingDatePicker
upcomingDates={upcomingDates}
value={value}
onChange={handleDayClick}
onMonthChange={handleMonthChange}
/>
) : null}

{isRangeVariant ? (
<RangeDatePicker
isDateRangeStyle={useDateRangeStyle}
shouldShowYearPicker={shouldShowYearPicker}
startMonth={startMonth}
endMonth={endMonth}
modifiers={{ ...modifiers, ...rangeModifier }}
getTooltip={getTooltip}
disabledDays={disabledDays}
getDayContent={getDayContent}
value={value}
handleDayClick={handleDayClick}
handleStartMonthChange={handleStartMonthChange}
handleEndMonthChange={handleEndMonthChange}
handleTodayClick={handleTodayClick}
selectedDays={selectedDays}
selectedDays={selectedDaysValues}
locale={locale ?? contextLocale}
timezoneName={timezoneName}
{...rest}
/>
) : (
<LocalizedDayPicker
className={clsx(
"ui-date-picker rounded-lg pt-3",
useDateRangeStyle ? "date-range-picker" : null,
useDateSelectedRangeStyle ? "date-range-picker max-w-[400px]" : null,
getDayContent ? "has-custom-content" : null,
modifiers.waitlist ? "has-custom-content" : null,
)}
todayButton="Today"
selectedDays={selectedDays}
selectedDays={selectedDaysValues}
month={currentMonth}
modifiers={{ ...modifiers, ...rangeModifier }}
disabledDays={disabledDays}
captionElement={CaptionElement}
renderDay={renderDay}
navbarElement={NavbarElement}
onDayClick={handleDayClick}
onMonthChange={handleMonthChange}
onTodayButtonClick={handleTodayClick}
{...rest}
/>
)}
</div>

{components.Footer ? <components.Footer /> : null}

{shouldShowRelativeRanges && (
<div className="max-w-200 ">
<div className="ml-auto w-5/12">
<RelativeDateRange
value={rangeName}
ranges={ranges}
timezoneName={timezoneName}
isFutureDatesAllowed={isFutureDatesAllowed}
onChange={handleRelativeRangeChanged}
onSubmit={onSubmitDateRange}
/>
</div>
</div>
)}
</>
);
};

Check warning on line 338 in src/components/DatePicker/DatePicker.jsx

View workflow job for this annotation

GitHub Actions / View Lint Report

src/components/DatePicker/DatePicker.jsx#L29-L338

[complexity] Arrow function has a complexity of 27. Maximum allowed is 25.

DatePicker.propTypes = {
variant: PropTypes.oneOf(Object.keys(variants)),
value: PropTypes.objectOf(Date),
selectedDays: PropTypes.objectOf(Date),
upcomingDates: PropTypes.arrayOf(Date),
onChange: PropTypes.func.isRequired,
onMonthChange: PropTypes.func,
disabledDays: PropTypes.oneOfType([PropTypes.object, PropTypes.array, PropTypes.func]),
shouldShowYearPicker: PropTypes.bool,
shouldShowMonthSelector: PropTypes.bool,
isDateRangeStyle: PropTypes.bool,
isRangeVariant: PropTypes.bool,
getDayContent: PropTypes.func,
Expand Down
27 changes: 16 additions & 11 deletions src/components/DatePicker/DatePickerPopover.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,14 @@ import { formatDate } from "../../helpers/date";
import { Input } from "../Forms/Input";
import { Popover } from "../Popover/Popover";
import { DatePicker } from "./DatePicker";
import { MonthPicker } from "./MonthPicker";

export const DatePickerPopover = ({
value,
variant = "single",
dateFormat = "ddd, LL",
placeholder = "Select Date",
pickerType = "day",
onChange,
children,
classNames = {},
Expand Down Expand Up @@ -79,17 +81,20 @@ export const DatePickerPopover = ({
)}

<Popover.Content className="pr-1">
{isVisible && (
<DatePicker
variant={variant}
getDayContent={getDayContent}
value={value}
components={components}
onChange={handleChange}
onSubmitDateRange={handleSubmitDateRange}
{...rest}
/>
)}
{isVisible &&
(pickerType === "month" ? (
<MonthPicker value={value} onChange={handleChange} {...rest} />
) : (
<DatePicker
variant={variant}
getDayContent={getDayContent}
value={value}
components={components}
onChange={handleChange}
onSubmitDateRange={handleSubmitDateRange}
{...rest}
/>
))}
</Popover.Content>
</Popover>
);
Expand Down
73 changes: 73 additions & 0 deletions src/components/DatePicker/MonthGrid.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import React, { useContext } from "react";
import dayjs from "dayjs";
import PropTypes from "prop-types";
import clsx from "clsx";
import { Context } from "../Provider";
import { ChevronLeftIcon, ChevronRightIcon } from "../../icons";
import { Button } from "../Buttons/Button";
import { ChevronButton } from "./NavbarElement";

const today = dayjs();

export const MonthGrid = ({ year, value, onChange, handleYearChange, locale, handleClear, handleToday }) => {
const { locale: contextLocale } = useContext(Context);
const months = [...Array.from({ length: 12 }).keys()].map((m) =>
today
.locale(locale ?? contextLocale)
.month(m)
.format("MMM"),
);

const handleMonthSelect = (monthIndex) => {
const newDate = new Date(year, monthIndex);
onChange(newDate);
};

return (
<>
<div className="mb-4 flex items-center justify-between">
<ChevronButton onClick={() => handleYearChange(-1)}>
<ChevronLeftIcon />
</ChevronButton>
<span className="text-lg font-bold">{year}</span>
<ChevronButton onClick={() => handleYearChange(1)}>
<ChevronRightIcon />
</ChevronButton>
</div>

<div className="grid grid-cols-4 gap-2">
{months.map((month, index) => (
<button
key={month}
type="button"
className={clsx(
value && value.getMonth() === index && value.getFullYear() === year
? "bg-blue-dark text-white"
: "text-black",
"rounded-md p-4 text-center hover:bg-blue-dark hover:text-white",
)}
onClick={() => handleMonthSelect(index)}
>
{month}
</button>
))}
</div>

<div className="mt-2 flex justify-between border-t border-gray-lighter pt-2">
<Button size="small" color="secondary" variant="outline" onClick={handleClear}>
Clear
</Button>
<Button size="small" color="secondary" variant="outline" onClick={handleToday}>
Today
</Button>
</div>
</>
);
};

MonthGrid.propTypes = {
year: PropTypes.number.isRequired,
value: PropTypes.objectOf(Date),
onChange: PropTypes.func.isRequired,
locale: PropTypes.string,
};
41 changes: 41 additions & 0 deletions src/components/DatePicker/MonthPicker.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import React, { useState } from "react";
import PropTypes from "prop-types";
import { MonthGrid } from "./MonthGrid";

export const MonthPicker = ({ value, onChange, locale }) => {
const [year, setYear] = useState(new Date(value).getFullYear());

const handleYearChange = (offset) => {
setYear(year + offset);
};

const handleToday = () => {
const today = new Date();
setYear(today.getFullYear());
onChange(today);
};

const handleClear = () => {
onChange(value);
};

return (
<div className="ui-month-picker min-w-72 max-w-xs rounded-lg bg-white">
<MonthGrid
year={year}
value={value}
locale={locale}
handleClear={handleClear}
handleToday={handleToday}
handleYearChange={handleYearChange}
onChange={onChange}
/>
</div>
);
};

MonthPicker.propTypes = {
value: PropTypes.objectOf(Date),
onChange: PropTypes.func,
locale: PropTypes.string,
};
65 changes: 65 additions & 0 deletions src/components/DatePicker/MonthSelector.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import PropTypes from "prop-types";
import React, { useState } from "react";
import dayjs from "dayjs";
import { ChevronDownIcon } from "../../icons";
import { Popover } from "../Popover/Popover";
import { MonthGrid } from "./MonthGrid";

export const MonthSelector = ({ date, locale, onChange, currentMonth }) => {
const [isVisible, setIsVisible] = useState(false);
const [year, setYear] = useState(new Date(currentMonth).getFullYear());

const toggleVisibility = () => {
setIsVisible(!isVisible);
};

const handleMonthSelect = (newDate) => {
onChange(newDate);
setIsVisible(false);
};

const handleYearChange = (offset) => {
setYear(year + offset);
};

const handleClear = () => {
setIsVisible(false);
};

const handleToday = () => {
onChange(new Date());
setIsVisible(false);
};

return (
<span className="DayPicker-Caption">
<span className="inline-block">
<Popover visible={isVisible} distance={5} skidding={60} placement="bottom" onClickOutside={handleClear}>
<div
className="mt-2 min-w-40 cursor-pointer items-center justify-between text-left font-bold"
onClick={toggleVisibility}
>
<span className="pr-1 text-lg">{dayjs(date).locale(locale).format("MMMM YYYY")}</span>
<ChevronDownIcon />
</div>
<Popover.Content className="p-2">
<MonthGrid
year={year}
value={date}
locale={locale}
handleClear={handleClear}
handleToday={handleToday}
handleYearChange={handleYearChange}
onChange={handleMonthSelect}
/>
</Popover.Content>
</Popover>
</span>
</span>
);
};

MonthSelector.propTypes = {
date: PropTypes.objectOf(Date).isRequired,
onChange: PropTypes.func.isRequired,
};
2 changes: 1 addition & 1 deletion src/components/DatePicker/NavbarElement.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ NavbarElement.propTypes = {
showPreviousButton: PropTypes.bool,
};

const ChevronButton = ({ isVisible = true, onClick, children }) => {
export const ChevronButton = ({ isVisible = true, onClick, children }) => {
return (
<button
type="button"
Expand Down
1 change: 1 addition & 0 deletions src/icons/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ export { ButtonCodeIcon } from "./src/ButtonCodeIcon";
export { CakeIcon } from "./src/CakeIcon";
export { CalendarDayIcon } from "./src/CalendarDayIcon";
export { CalendarIcon } from "./src/CalendarIcon";
export { CalendarListIcon } from "./src/CalendarListIcon";
export { CalendarMonthIcon } from "./src/CalendarMonthIcon";
export { CalendarWeekIcon } from "./src/CalendarWeekIcon";
export { CapacityIcon } from "./src/CapacityIcon";
Expand Down
Loading