Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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 .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,15 @@ module.exports = {
'import/no-unresolved': 0,
'import/no-extraneous-dependencies': 0,
'no-shadow': 0,
'no-nested-ternary': 0,
'react/prop-types': 0,
'react/require-default-props': 0,
'react/jsx-fragments': 0,
'react/jsx-filename-extension': [
2,
{ extensions: ['.js', '.jsx', '.ts', '.tsx'] },
],
'react/jsx-curly-newline': 0,
'jsx-a11y/no-noninteractive-element-interactions': 0,
'react/jsx-props-no-spreading': 0,
'@typescript-eslint/explicit-function-return-type': [
Expand Down
107 changes: 107 additions & 0 deletions example/src/stories/DateSelect.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import { Centering, DateSelect } from '@solved-ac/ui-react'
import { Meta, StoryObj } from '@storybook/react'
import React from 'react'

export default {
title: 'Components/DateSelect',
component: DateSelect,
argTypes: {
locale: {
control: 'text',
description: 'The locale to use',
},
weekStartsOn: {
defaultValue: 0,
options: [0, 1, 2, 3, 4, 5, 6],
control: {
type: 'select',
},
description: 'The day of the week to start on',
},
chunks: {
defaultValue: 1,
options: [1, 2, 3, 4, 5, 6],
control: {
type: 'select',
},
description: 'The number of months to display at once',
},
annotations: {
control: 'array',
description: 'The annotations to display',
defaultValue: [],
},
},
} as Meta<typeof DateSelect>

type Story = StoryObj<typeof DateSelect>

export const Default: Story = {
render: (args) => (
<Centering>
<DateSelect {...args} />
</Centering>
),
}

const DAY = 1000 * 60 * 60 * 24

export const Annotations: Story = {
render: (args) => (
<Centering>
<DateSelect {...args} />
</Centering>
),
args: {
annotations: [
{
title: "I'm a title",
color: '#e3f44f',
start: new Date().toISOString().split('T')[0],
end: new Date(Date.now() + DAY * 7).toISOString().split('T')[0],
},
{
title: "I'm a title",
color: '#c1ecff',
start: new Date(Date.now() - DAY * 17).toISOString().split('T')[0],
end: new Date(Date.now() - DAY * 5).toISOString().split('T')[0],
},
{
title: "I'm a title",
color: '#ffd2bd',
start: new Date(Date.now() + DAY * 4).toISOString().split('T')[0],
end: new Date(Date.now() + DAY * 16).toISOString().split('T')[0],
},
{
title: "I'm a title",
color: '#ffc8fb',
start: new Date(Date.now() + DAY * 9).toISOString().split('T')[0],
end: new Date(Date.now() + DAY * 9).toISOString().split('T')[0],
},
{
title: "I'm a title",
color: '#f9ffc1',
start: new Date(Date.now() + DAY * 16).toISOString().split('T')[0],
end: new Date(Date.now() + DAY * 17).toISOString().split('T')[0],
},
{
title: "I'm a title",
color: '#d7ffc0',
start: new Date(Date.now() + DAY * 24).toISOString().split('T')[0],
end: new Date(Date.now() + DAY * 28).toISOString().split('T')[0],
},
{
title: "I'm a title",
color: '#b9b7b4',
start: new Date(Date.now() + DAY * 21).toISOString().split('T')[0],
end: new Date(Date.now() + DAY * 25).toISOString().split('T')[0],
},
{
title: "I'm a title",
color: '#ffcbcb',
start: new Date(Date.now() + DAY * 34).toISOString().split('T')[0],
end: new Date(Date.now() + DAY * 36).toISOString().split('T')[0],
},
],
},
}
124 changes: 124 additions & 0 deletions src/components/$DateSelect/DateSelect.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
import styled from '@emotion/styled'
import React, { ElementType, useEffect, useState } from 'react'
import { PP, PR } from '../../types/PolymorphicElementProps'
import { forwardRefWithGenerics } from '../../utils/ref'
import { DateSelectContext } from './DateSelectContext'
import { DateSelectMonthView } from './DateSelectMonthView'

export interface DateRange {
start: string
end: string
}

export type DateSelectValues =
| {
type: 'date'
value: string
onChange?: (value: string) => void
}
| {
type: 'date-range'
value: DateRange
onChange?: (value: DateRange) => void
}

export interface DateSelectAnnotation extends DateRange {
title?: string
color?: string
}

export type DateSelectProps = DateSelectValues & {
annotations?: DateSelectAnnotation[]
maxAnnotationsPerDay?: number
weekStartsOn?: 0 | 1 | 2 | 3 | 4 | 5 | 6
locale?: string
chunks?: number
}

export type DateSelectMode = 'year' | 'month' | 'date'
export type CursorMode = 'select' | 'selectStart' | 'selectEnd'

export type DateSelectCursor =
| {
mode: 'select'
hover: Date | null
}
| {
mode: 'selectStart'
hover: Date | null
}
| {
mode: 'selectEnd'
valueStart: Date
hover: Date | null
}

// const toDateString = (date: Date): string => {
// return date.toISOString().split('T')[0]
// }

// const fromDateString = (date: string): Date => {
// return new Date(date)
// }

const DateSelectContainer = styled.div`
user-select: none;
display: flex;
gap: 1em;
`

export const DateSelect = forwardRefWithGenerics(
<T extends ElementType>(props: PP<T, DateSelectProps>, ref?: PR<T>) => {
const {
type,
value,
onChange,
annotations = [],
maxAnnotationsPerDay = annotations.length ? 3 : 0,
weekStartsOn = 0,
locale,
chunks = 1,
...rest
} = props
// const theme = useTheme()

const [currentMode, setCurrentMode] = useState<DateSelectMode>('date')
const [selectState, setSelectState] = useState<DateSelectCursor>({
mode: type === 'date' ? ('select' as const) : ('selectStart' as const),
hover: null,
})
const [cursorDate, setCursorDate] = useState<Date>(new Date())

useEffect(() => {
setSelectState({
mode: type === 'date' ? ('select' as const) : ('selectStart' as const),
hover: null,
})
}, [type])

// const selectedYear = selectedDate.getFullYear()
// const selectedMonth = selectedDate.getMonth()

return (
<DateSelectContext.Provider value={props}>
<DateSelectContainer {...rest} ref={ref}>
{currentMode === 'date' &&
new Array(chunks).fill(0).map((_, index) => (
<DateSelectMonthView
// eslint-disable-next-line react/no-array-index-key
key={index}
offset={index - Math.floor(chunks / 2)}
cursorDate={cursorDate}
setCursorDate={setCursorDate}
selectState={selectState}
setSelectState={setSelectState}
setModeToMonth={() => setCurrentMode('month')}
firstMonth={index === 0}
lastMonth={index === chunks - 1}
/>
))}
</DateSelectContainer>
</DateSelectContext.Provider>
)
}
)
13 changes: 13 additions & 0 deletions src/components/$DateSelect/DateSelectContext.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import React, { useContext } from 'react'
import { DateSelectProps } from './DateSelect'

export const DateSelectContext = React.createContext<DateSelectProps>({
type: 'date',
value: '2020-06-05',
onChange: () => {
/* no-op */
},
})

export const useDateSelectContext = (): DateSelectProps =>
useContext(DateSelectContext)
Loading