diff --git a/apps/website/src/components/datepicker/datepicker.tsx b/apps/website/src/components/datepicker/datepicker.tsx
new file mode 100644
index 00000000..c2a0d54f
--- /dev/null
+++ b/apps/website/src/components/datepicker/datepicker.tsx
@@ -0,0 +1,45 @@
+import { Calendar } from '@status-im/components/src/calendar/calendar'
+import { Popover } from '@status-im/components/src/popover'
+import { EditIcon } from '@status-im/icons'
+
+import { formatDate } from '../chart/utils/format-time'
+
+import type { DateRange } from '@status-im/components/src/calendar/calendar'
+
+type Props = {
+ selected?: DateRange
+ onSelect: (selected?: DateRange) => void
+}
+
+const DatePicker = (props: Props) => {
+ const { selected, onSelect } = props
+
+ return (
+
+
+
+
+
+
+
+
+ )
+}
+
+export { DatePicker }
diff --git a/apps/website/src/pages/insights/epics/index.tsx b/apps/website/src/pages/insights/epics/index.tsx
index ac9cc533..c6aab954 100644
--- a/apps/website/src/pages/insights/epics/index.tsx
+++ b/apps/website/src/pages/insights/epics/index.tsx
@@ -1,3 +1,5 @@
+import { useState } from 'react'
+
import { IconButton, Shadow, Tag, Text } from '@status-im/components'
import {
DoneIcon,
@@ -7,9 +9,11 @@ import {
SortIcon,
} from '@status-im/icons'
+import { DatePicker } from '@/components/datepicker/datepicker'
import { EpicOverview } from '@/components/epic-overview'
import { InsightsLayout } from '@/layouts/insights-layout'
+import type { DateRange } from '@status-im/components/src/calendar/calendar'
import type { Page } from 'next'
const epics = [
@@ -27,6 +31,8 @@ const epics = [
]
const EpicsPage: Page = () => {
+ const [selectedDates, setSelectedDates] = useState()
+
return (
@@ -53,6 +59,8 @@ const EpicsPage: Page = () => {
))}
+
+
)
}
diff --git a/packages/components/package.json b/packages/components/package.json
index bf90cb04..307e2267 100644
--- a/packages/components/package.json
+++ b/packages/components/package.json
@@ -46,8 +46,10 @@
"@tamagui/react-native-media-driver": "1.11.1",
"@tamagui/shorthands": "1.11.1",
"@tamagui/theme-base": "1.11.1",
+ "date-fns": "^2.30.0",
"expo-blur": "^12.2.2",
"expo-linear-gradient": "^12.1.2",
+ "react-day-picker": "^8.7.1",
"tamagui": "1.11.1",
"zustand": "^4.3.7"
},
diff --git a/packages/components/src/calendar/calendar.css b/packages/components/src/calendar/calendar.css
new file mode 100644
index 00000000..3628f4e0
--- /dev/null
+++ b/packages/components/src/calendar/calendar.css
@@ -0,0 +1,354 @@
+.rdp {
+ --rdp-cell-size: 32px;
+ --rdp-caption-font-size: 15px;
+ --rdp-accent-color: #2a4af5;
+ --rdp-background-color: #e7edff;
+ --rdp-background-color-selected-secondary: #f5f6f8;
+ --rdp-hover-color: #f5f6f8;
+ --rdp-hover-color-darker: #f0f2f5;
+ --rdp-accent-color-dark: #223bc4;
+ --rdp-outline: 2px solid var(--rdp-accent-color); /* Outline border for focused elements */
+ --rdp-outline-selected: 3px solid var(--rdp-accent-color); /* Outline border for focused _and_ selected elements */
+ --rdp-text-color: #09101c;
+ color: var(--rdp-text-color);
+ font-family: Inter, sans-serif;
+ font-weight: 500;
+ user-select: none;
+}
+
+/* Hide elements for devices that are not screen readers */
+.rdp-vhidden {
+ box-sizing: border-box;
+ padding: 0;
+ margin: 0;
+ background: transparent;
+ border: 0;
+ -moz-appearance: none;
+ -webkit-appearance: none;
+ appearance: none;
+ position: absolute !important;
+ top: 0;
+ width: 1px !important;
+ height: 1px !important;
+ padding: 0 !important;
+ overflow: hidden !important;
+ clip: rect(1px, 1px, 1px, 1px) !important;
+ border: 0 !important;
+}
+
+/* Buttons */
+.rdp-button_reset {
+ appearance: none;
+ position: relative;
+ margin: 0;
+ padding: 0;
+ cursor: default;
+ color: inherit;
+ background: none;
+ font: inherit;
+
+ -moz-appearance: none;
+ -webkit-appearance: none;
+}
+
+.rdp-button_reset:focus-visible {
+ /* Make sure to reset outline only when :focus-visible is supported */
+ outline: none;
+}
+
+.rdp-button {
+ border: 2px solid transparent;
+ font-size: 0.8125rem;
+}
+
+.rdp-button[disabled]:not(.rdp-day_selected) {
+ opacity: 0.25;
+}
+
+.rdp-button:not([disabled]) {
+ cursor: pointer;
+}
+
+.rdp-button:focus-visible:not([disabled]) {
+ color: inherit;
+ background-color: var(--rdp-background-color);
+ border: var(--rdp-outline);
+}
+
+.rdp-button:hover:not([disabled]):not(.rdp-day_selected) {
+ background-color: var(--rdp-hover-color);
+}
+
+.rdp-months {
+ display: flex;
+}
+
+.rdp-month {
+ margin: 0 1em;
+}
+
+.rdp-month:first-child {
+ margin-left: 0;
+}
+
+.rdp-month:last-child {
+ margin-right: 0;
+}
+
+.rdp-table {
+ margin: 0;
+ max-width: calc(var(--rdp-cell-size) * 7);
+ border-collapse: separate;
+ border-spacing: 0 2px;
+ padding: 0 0.75rem 0.625rem 0.75rem;
+}
+
+.rdp-with_weeknumber .rdp-table {
+ max-width: calc(var(--rdp-cell-size) * 8);
+ border-collapse: collapse;
+}
+
+.rdp-caption {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 0;
+ text-align: left;
+}
+
+.rdp-multiple_months .rdp-caption {
+ position: relative;
+ display: block;
+ text-align: center;
+}
+
+.rdp-caption_dropdowns {
+ position: relative;
+ display: inline-flex;
+}
+
+.rdp-caption_label {
+ position: relative;
+ z-index: 1;
+ display: inline-flex;
+ align-items: center;
+ margin: 0;
+ white-space: nowrap;
+ color: currentColor;
+ font-family: inherit;
+ font-size: var(--rdp-caption-font-size);
+ font-weight: bold;
+}
+
+.rdp-nav {
+ white-space: nowrap;
+ padding: 0.375rem 0.375rem 0 0.75rem;
+}
+
+.rdp-multiple_months .rdp-caption_start .rdp-nav {
+ position: absolute;
+ top: 50%;
+ left: 0;
+ transform: translateY(-50%);
+}
+
+.rdp-multiple_months .rdp-caption_end .rdp-nav {
+ position: absolute;
+ top: 50%;
+ right: 0;
+ transform: translateY(-50%);
+}
+
+.rdp-nav_button {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ width: 20px;
+ height: 20px;
+ padding: 0.25em;
+ border-radius: 10px;
+}
+
+/* ---------- */
+/* Dropdowns */
+/* ---------- */
+
+.rdp-dropdown_year,
+.rdp-dropdown_month {
+ position: relative;
+ display: inline-flex;
+ align-items: center;
+}
+
+.rdp-dropdown {
+ appearance: none;
+ position: absolute;
+ z-index: 2;
+ top: 0;
+ bottom: 0;
+ left: 0;
+ width: 100%;
+ margin: 0;
+ padding: 0;
+ cursor: inherit;
+ opacity: 0;
+ border: none;
+ background-color: transparent;
+ font-family: inherit;
+ font-size: inherit;
+ line-height: inherit;
+}
+
+.rdp-dropdown[disabled] {
+ opacity: unset;
+ color: unset;
+}
+
+.rdp-dropdown:focus-visible:not([disabled]) + .rdp-caption_label {
+ background-color: var(--rdp-background-color);
+ border: var(--rdp-outline);
+ border-radius: 6px;
+}
+
+.rdp-dropdown_icon {
+ margin: 0 0 0 5px;
+}
+
+.rdp-head {
+ border: 0;
+}
+
+.rdp-head_row,
+.rdp-row {
+ height: 100%;
+ padding-bottom: 2px;
+}
+
+.rdp-head_cell {
+ vertical-align: middle;
+ font-size: 0.8125em;
+ font-weight: 500;
+
+ text-align: center;
+ height: var(--rdp-cell-size);
+ padding: 0;
+
+ line-height: 140%;
+ color: #647084;
+ letter-spacing: -0.003em;
+}
+
+.rdp-tbody {
+ border: 0;
+}
+
+.rdp-tfoot {
+ margin: 0.5em;
+}
+
+.rdp-cell {
+ width: var(--rdp-cell-size);
+ height: var(--rdp-cell-size);
+ text-align: center;
+ padding: 0;
+}
+
+.rdp-cell_selected_start {
+ border-top-left-radius: 10px;
+ border-bottom-left-radius: 10px;
+ background-color: var(--rdp-background-color-selected-secondary);
+}
+
+.rdp-cell_selected_end {
+ border-top-right-radius: 10px;
+ border-bottom-right-radius: 10px;
+ background-color: var(--rdp-background-color-selected-secondary);
+}
+
+.rdp-cell_selected_range {
+ background-color: var(--rdp-background-color-selected-secondary);
+}
+
+.rdp-weeknumber {
+ font-size: 0.75em;
+}
+
+.rdp-weeknumber,
+.rdp-day {
+ display: flex;
+ overflow: hidden;
+ align-items: center;
+ justify-content: center;
+ box-sizing: border-box;
+ width: var(--rdp-cell-size);
+ max-width: var(--rdp-cell-size);
+ height: var(--rdp-cell-size);
+ margin: 0;
+ border: 2px solid transparent;
+ border-radius: 10px;
+}
+
+.rdp-day_today:not(.rdp-day_outside) {
+ position: relative;
+}
+.rdp-day_today:not(.rdp-day_outside)::after {
+ content: '';
+ position: absolute;
+ bottom: 3px;
+ left: 50%;
+ width: 4px;
+ height: 2px;
+ transform: translateX(-50%);
+
+ border-radius: 10px;
+ background-color: var(--rdp-accent-color);
+}
+
+.rdp-day_selected {
+ opacity: 1;
+ background-color: var(--rdp-accent-color);
+ color: var(--rdp-background-color);
+ transition: all 150ms ease-in-out;
+}
+
+.rdp-day_selected:focus-visible,
+.rdp-day_selected:hover {
+ opacity: 1;
+ background-color: var(--rdp-accent-color-dark);
+ border-radius: 10px;
+}
+
+.rdp-day_outside {
+ opacity: 0.3;
+}
+
+.rdp-day_selected:focus-visible {
+ /* Since the background is the same use again the outline */
+ outline: var(--rdp-outline);
+ outline-offset: 2px;
+ z-index: 1;
+}
+
+.rdp[dir='rtl'] .rdp-day_range_start:not(.rdp-day_range_end) {
+ border-top-left-radius: 0;
+ border-bottom-left-radius: 0;
+}
+
+.rdp[dir='rtl'] .rdp-day_range_end:not(.rdp-day_range_start) {
+ border-top-right-radius: 0;
+ border-bottom-right-radius: 0;
+}
+
+.rdp-day_range_end.rdp-day_range_start {
+ border-radius: 10px;
+}
+
+.rdp-day_range_middle {
+ color: var(--rdp-text-color);
+ background-color: var(--rdp-background-color-selected-secondary);
+}
+
+.rdp-day_range_middle:hover {
+ color: var(--rdp-text-color);
+ background-color: var(--rdp-hover-color-darker);
+}
diff --git a/packages/components/src/calendar/calendar.stories.tsx b/packages/components/src/calendar/calendar.stories.tsx
new file mode 100644
index 00000000..7066c2ed
--- /dev/null
+++ b/packages/components/src/calendar/calendar.stories.tsx
@@ -0,0 +1,86 @@
+import { useState } from 'react'
+
+import { Stack } from '@tamagui/core'
+import { format } from 'date-fns'
+
+import { Text } from '../text'
+import { Calendar } from './calendar'
+
+import type { Meta, StoryObj } from '@storybook/react'
+import type { DateRange } from 'react-day-picker'
+
+// More on how to set up stories at: https://storybook.js.org/docs/7.0/react/writing-stories/introduction
+const meta: Meta = {
+ component: Calendar,
+ args: {},
+ argTypes: {
+ disabled: {
+ defaultValue: false,
+ },
+ },
+}
+
+// More on writing stories with args: https://storybook.js.org/docs/7.0/react/writing-stories/args
+type Story = StoryObj
+
+const SingleCalendarWithHooks = () => {
+ const [selected, setSelected] = useState()
+
+ return (
+ <>
+
+
+
+ Selected date: {selected && format(selected, 'do MMM yyyy')}
+
+
+ >
+ )
+}
+
+const MultipleCalendarWithHooks = () => {
+ const [selected, setSelected] = useState()
+ return (
+ <>
+
+
+
+ Selected dates:{' '}
+ {selected?.map(date => format(date, 'do MMM yyyy')).join(', ')}
+
+
+ >
+ )
+}
+
+const RangeCalendarWithHooks = () => {
+ const [selected, setSelected] = useState()
+
+ return (
+ <>
+
+
+
+ Start date: {selected?.from && format(selected.from, 'do MMM yyyy')}
+
+
+ End Date: {selected?.to && format(selected.to, 'do MMM yyyy')}
+
+
+ >
+ )
+}
+
+export const SinglePicker: Story = {
+ render: () => ,
+}
+
+export const MultiplePicker: Story = {
+ render: () => ,
+}
+
+export const RangePicker: Story = {
+ render: () => ,
+}
+
+export default meta
diff --git a/packages/components/src/calendar/calendar.tsx b/packages/components/src/calendar/calendar.tsx
new file mode 100644
index 00000000..ffa186b5
--- /dev/null
+++ b/packages/components/src/calendar/calendar.tsx
@@ -0,0 +1,34 @@
+import './calendar.css'
+
+import { DayPicker } from 'react-day-picker'
+
+import { Shadow } from '../shadow'
+import { CustomCaption } from './components/caption'
+import { CustomRow } from './components/row'
+
+import type { DateRange, DayPickerProps } from 'react-day-picker'
+
+type Props = DayPickerProps
+
+const Calendar = (props: Props): JSX.Element => {
+ return (
+
+
+
+ )
+}
+
+export { Calendar }
+export type { Props as CalendarProps, DateRange }
diff --git a/packages/components/src/calendar/components/caption.tsx b/packages/components/src/calendar/components/caption.tsx
new file mode 100644
index 00000000..3a9d27d9
--- /dev/null
+++ b/packages/components/src/calendar/components/caption.tsx
@@ -0,0 +1,44 @@
+import { ChevronLeftIcon, ChevronRightIcon } from '@status-im/icons'
+import { Stack } from '@tamagui/core'
+import { format } from 'date-fns'
+import { useNavigation } from 'react-day-picker'
+
+import { IconButton } from '../../icon-button'
+
+import type { CaptionProps } from 'react-day-picker'
+
+const CustomCaption = (props: CaptionProps): JSX.Element => {
+ const { goToMonth, nextMonth, previousMonth } = useNavigation()
+ return (
+
+
+
+ {format(props.displayMonth, 'MMM yyy')}
+
+
+
+ }
+ disabled={!previousMonth}
+ onPress={() => previousMonth && goToMonth(previousMonth)}
+ />
+ }
+ disabled={!nextMonth}
+ onPress={() => nextMonth && goToMonth(nextMonth)}
+ />
+
+
+
+ )
+}
+
+export { CustomCaption }
diff --git a/packages/components/src/calendar/components/row.tsx b/packages/components/src/calendar/components/row.tsx
new file mode 100644
index 00000000..149a970d
--- /dev/null
+++ b/packages/components/src/calendar/components/row.tsx
@@ -0,0 +1,54 @@
+import { getUnixTime, isEqual } from 'date-fns'
+import { Day, useDayPicker } from 'react-day-picker'
+
+import type { DateRange, RowProps } from 'react-day-picker'
+
+const CustomRow = (props: RowProps): JSX.Element => {
+ const { styles, classNames, selected } = useDayPicker()
+
+ const castSelected = selected as DateRange
+
+ return (
+
+ {props.dates.map(date => {
+ const isSelectedStartDate =
+ castSelected?.from && isEqual(date, castSelected.from)
+ const isSelectedEndDate =
+ castSelected?.to && isEqual(date, castSelected.to)
+
+ const cellClassNames = () => {
+ if (isSelectedStartDate) {
+ return classNames.cell + ' ' + 'rdp-cell_selected_start'
+ }
+ if (isSelectedEndDate) {
+ return classNames.cell + ' ' + 'rdp-cell_selected_end'
+ }
+
+ if (
+ castSelected?.from &&
+ castSelected?.to &&
+ date > castSelected.from &&
+ date < castSelected.to
+ ) {
+ return classNames.cell + ' ' + 'rdp-cell_selected_range'
+ }
+
+ return classNames.cell
+ }
+
+ return (
+
+
+ |
+ )
+ })}
+
+ )
+}
+
+export { CustomRow }
diff --git a/packages/components/src/calendar/index.tsx b/packages/components/src/calendar/index.tsx
new file mode 100644
index 00000000..e8afcc58
--- /dev/null
+++ b/packages/components/src/calendar/index.tsx
@@ -0,0 +1 @@
+export { Calendar } from './calendar'
diff --git a/packages/components/src/index.tsx b/packages/components/src/index.tsx
index d47ecf59..ef945ee6 100644
--- a/packages/components/src/index.tsx
+++ b/packages/components/src/index.tsx
@@ -1,6 +1,7 @@
export * from './anchor-actions'
export * from './avatar'
export * from './button'
+export * from './calendar'
export * from './community'
export * from './composer'
export * from './context-tag'
@@ -21,6 +22,5 @@ export * from './tag'
export * from './text'
export * from './toast'
export * from './user-list'
-
// eslint-disable-next-line simple-import-sort/exports
export { config } from './tamagui.config'
diff --git a/yarn.lock b/yarn.lock
index 181f973a..ddfd3e9b 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1818,6 +1818,13 @@
dependencies:
regenerator-runtime "^0.13.11"
+"@babel/runtime@^7.21.0":
+ version "7.21.5"
+ resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.21.5.tgz#8492dddda9644ae3bda3b45eabe87382caee7200"
+ integrity sha512-8jI69toZqqcsnqGGqwGS4Qb1VwLOEp4hz+CXPywcvjs60u3B4Pom/U/7rm4W8tMOYEB+E9wgD0mW1l3r8qlI9Q==
+ dependencies:
+ regenerator-runtime "^0.13.11"
+
"@babel/template@7.18.10", "@babel/template@^7.18.10":
version "7.18.10"
resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.18.10.tgz#6f9134835970d1dbf0835c0d100c9f38de0c5e71"
@@ -10246,6 +10253,13 @@ datastore-core@^8.0.1:
it-take "^1.0.1"
uint8arrays "^3.0.0"
+date-fns@^2.30.0:
+ version "2.30.0"
+ resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.30.0.tgz#f367e644839ff57894ec6ac480de40cae4b0f4d0"
+ integrity sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==
+ dependencies:
+ "@babel/runtime" "^7.21.0"
+
dayjs@^1.8.15:
version "1.11.7"
resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.11.7.tgz#4b296922642f70999544d1144a2c25730fce63e2"
@@ -16867,6 +16881,11 @@ react-colorful@^5.1.2:
resolved "https://registry.yarnpkg.com/react-colorful/-/react-colorful-5.6.1.tgz#7dc2aed2d7c72fac89694e834d179e32f3da563b"
integrity sha512-1exovf0uGTGyq5mXQT0zgQ80uvj2PCwvF8zY1RN9/vbJVSjSo3fsB/4L3ObbF7u70NduSiK4xu4Y6q1MHoUGEw==
+react-day-picker@^8.7.1:
+ version "8.7.1"
+ resolved "https://registry.yarnpkg.com/react-day-picker/-/react-day-picker-8.7.1.tgz#b8388c2afa69d4a6da4c5fde5323fae884acfe2f"
+ integrity sha512-Gv426AW8b151CZfh3aP5RUGztLwHB/EyJgWZ5iMgtzbFBkjHfG6Y66CIQFMWGLnYjsQ9DYSJRmJ5S0Pg5HWKjA==
+
react-devtools-core@4.24.0:
version "4.24.0"
resolved "https://registry.yarnpkg.com/react-devtools-core/-/react-devtools-core-4.24.0.tgz#7daa196bdc64f3626b3f54f2ff2b96f7c4fdf017"