[website][status-web] Calendar (#410)

* feat: adds calendar component to components package

* feat: add datepicker component to use in insights

* feat: clean calendar code and fix spacing
This commit is contained in:
marcelines 2023-06-13 11:33:14 +01:00 committed by GitHub
parent f54c22abf0
commit 1866ca8c42
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 648 additions and 1 deletions

View File

@ -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 (
<div className="sticky bottom-5 flex justify-center">
<Popover alignOffset={8} align="center" sideOffset={8}>
<button className="border-neutral-80/5 bg-blur-neutral-5/70 active inline-flex min-h-[30px] cursor-pointer items-center justify-center gap-2 rounded-xl border-solid pl-3 pr-2 uppercase text-neutral-100 backdrop-blur-sm">
<span className="text-blur-neutral-80/80 text-[13px] font-medium">
Filter between
</span>
<span className="text-[13px] font-medium text-neutral-100">
{`${selected?.from ? formatDate(selected.from) : 'Start Date'}${
selected?.to ? formatDate(selected.to) : 'End Date'
}`}
</span>
<div className="bg-neutral-80/5 h-full w-[1px]" />
<EditIcon size={20} color="$neutral-80-opa-40" />
</button>
<Popover.Content>
<Calendar
mode="range"
selected={selected}
onSelect={onSelect}
fixedWeeks
/>
</Popover.Content>
</Popover>
</div>
)
}
export { DatePicker }

View File

@ -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<DateRange>()
return (
<div className="space-y-4 p-10">
<Text size={27} weight="semibold">
@ -53,6 +59,8 @@ const EpicsPage: Page = () => {
</Shadow>
))}
</div>
<DatePicker selected={selectedDates} onSelect={setSelectedDates} />
</div>
)
}

View File

@ -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"
},

View File

@ -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);
}

View File

@ -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<typeof Calendar> = {
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<typeof Calendar>
const SingleCalendarWithHooks = () => {
const [selected, setSelected] = useState<Date>()
return (
<>
<Calendar mode="single" selected={selected} onSelect={setSelected} />
<Stack pt="$2">
<Text size={15} color="$neutral-50">
Selected date: {selected && format(selected, 'do MMM yyyy')}
</Text>
</Stack>
</>
)
}
const MultipleCalendarWithHooks = () => {
const [selected, setSelected] = useState<Date[]>()
return (
<>
<Calendar mode="multiple" selected={selected} onSelect={setSelected} />
<Stack pt="$2">
<Text size={15} color="$neutral-50">
Selected dates:{' '}
{selected?.map(date => format(date, 'do MMM yyyy')).join(', ')}
</Text>
</Stack>
</>
)
}
const RangeCalendarWithHooks = () => {
const [selected, setSelected] = useState<DateRange>()
return (
<>
<Calendar mode="range" selected={selected} onSelect={setSelected} />
<Stack pt="$2">
<Text size={15} color="$neutral-50">
Start date: {selected?.from && format(selected.from, 'do MMM yyyy')}
</Text>
<Text size={15} color="$neutral-50">
End Date: {selected?.to && format(selected.to, 'do MMM yyyy')}
</Text>
</Stack>
</>
)
}
export const SinglePicker: Story = {
render: () => <SingleCalendarWithHooks />,
}
export const MultiplePicker: Story = {
render: () => <MultipleCalendarWithHooks />,
}
export const RangePicker: Story = {
render: () => <RangeCalendarWithHooks />,
}
export default meta

View File

@ -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 (
<Shadow
borderWidth={1}
borderColor="$neutral-10"
borderRadius="$12"
display="inline-flex"
>
<DayPicker
{...props}
showOutsideDays
components={{
Row: CustomRow,
Caption: CustomCaption,
}}
/>
</Shadow>
)
}
export { Calendar }
export type { Props as CalendarProps, DateRange }

View File

@ -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 (
<div className="rdp-nav">
<div className="rdp-caption">
<div
className="rdp-caption_label"
aria-live="polite"
role="presentation"
>
{format(props.displayMonth, 'MMM yyy')}
</div>
<Stack flexDirection="row">
<IconButton
variant="ghost"
aria-label="Go to previous month"
icon={<ChevronLeftIcon size={20} />}
disabled={!previousMonth}
onPress={() => previousMonth && goToMonth(previousMonth)}
/>
<IconButton
variant="ghost"
aria-label="Go to next month"
icon={<ChevronRightIcon size={20} />}
disabled={!nextMonth}
onPress={() => nextMonth && goToMonth(nextMonth)}
/>
</Stack>
</div>
</div>
)
}
export { CustomCaption }

View File

@ -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 (
<tr className={classNames.row} style={styles.row}>
{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 (
<td
className={cellClassNames()}
style={styles.cell}
key={getUnixTime(date)}
role="presentation"
>
<Day displayMonth={props.displayMonth} date={date} />
</td>
)
})}
</tr>
)
}
export { CustomRow }

View File

@ -0,0 +1 @@
export { Calendar } from './calendar'

View File

@ -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'

View File

@ -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"