-
-
Notifications
You must be signed in to change notification settings - Fork 2.8k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(mobile): new journal date-picker
- Loading branch information
Showing
15 changed files
with
828 additions
and
47 deletions.
There are no files selected for viewing
11 changes: 11 additions & 0 deletions
11
packages/frontend/core/src/mobile/pages/workspace/detail/journal-date-picker/constants.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
export const CELL_HEIGHT = 34; | ||
export const TOTAL_WEEKS = 6; | ||
export const ROWS_GAP = 4; | ||
|
||
export const MONTH_VIEW_HEIGHT = | ||
TOTAL_WEEKS * CELL_HEIGHT + (TOTAL_WEEKS - 1) * ROWS_GAP; | ||
export const WEEK_VIEW_HEIGHT = CELL_HEIGHT; | ||
|
||
export const HORIZONTAL_SWIPE_THRESHOLD = 4 * CELL_HEIGHT; | ||
|
||
export const DATE_FORMAT = 'YYYY-MM-DD'; |
21 changes: 21 additions & 0 deletions
21
packages/frontend/core/src/mobile/pages/workspace/detail/journal-date-picker/context.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
import { createContext } from 'react'; | ||
|
||
export const JournalDatePickerContext = createContext<{ | ||
width: number; | ||
/** | ||
* Is used to determine the current date, not same as selected, | ||
* `is-current-month` is based on cursor | ||
*/ | ||
cursor: string; | ||
setCursor: (date: string) => void; | ||
selected: string; | ||
onSelect: (date: string) => void; | ||
withDotDates: Set<string | null | undefined>; | ||
}>({ | ||
width: window.innerWidth, | ||
cursor: '', | ||
setCursor: () => {}, | ||
selected: '', | ||
onSelect: () => {}, | ||
withDotDates: new Set(), | ||
}); |
40 changes: 40 additions & 0 deletions
40
packages/frontend/core/src/mobile/pages/workspace/detail/journal-date-picker/day-cell.css.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
import { bodyRegular } from '@toeverything/theme/typography'; | ||
import { cssVarV2 } from '@toeverything/theme/v2'; | ||
import { style } from '@vanilla-extract/css'; | ||
|
||
export const dayCell = style([ | ||
bodyRegular, | ||
{ | ||
position: 'relative', | ||
height: 34, | ||
minWidth: 34, | ||
padding: 4, | ||
borderRadius: 8, | ||
display: 'flex', | ||
alignItems: 'center', | ||
justifyContent: 'center', | ||
|
||
selectors: { | ||
'&[data-is-today=true]': {}, | ||
'&[data-is-current-month=false]': { | ||
color: cssVarV2('text/disable'), | ||
}, | ||
'&[data-is-selected=true]': { | ||
background: cssVarV2('button/primary'), | ||
color: cssVarV2('button/pureWhiteText'), | ||
fontWeight: 600, | ||
}, | ||
}, | ||
}, | ||
]); | ||
|
||
export const dot = style({ | ||
position: 'absolute', | ||
width: 3, | ||
height: 3, | ||
borderRadius: 3, | ||
background: cssVarV2('button/primary'), | ||
bottom: 3, | ||
left: '50%', | ||
transform: 'translateX(-50%)', | ||
}); |
39 changes: 39 additions & 0 deletions
39
packages/frontend/core/src/mobile/pages/workspace/detail/journal-date-picker/day-cell.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
import dayjs from 'dayjs'; | ||
import { memo, useContext, useMemo } from 'react'; | ||
|
||
import { DATE_FORMAT } from './constants'; | ||
import { JournalDatePickerContext } from './context'; | ||
import { dayCell, dot } from './day-cell.css'; | ||
|
||
export interface DayCellProps { | ||
date: string; | ||
} | ||
|
||
export const DayCell = memo(function DayCell({ date }: DayCellProps) { | ||
const { selected, onSelect, cursor, withDotDates } = useContext( | ||
JournalDatePickerContext | ||
); | ||
|
||
const dayjsObj = useMemo(() => dayjs(date), [date]); | ||
|
||
const isToday = dayjsObj.isSame(dayjs(), 'day'); | ||
const isSelected = dayjsObj.isSame(dayjs(selected), 'day'); | ||
const isCurrentMonth = dayjsObj.isSame(dayjs(cursor), 'month'); | ||
const day = dayjsObj.get('date'); | ||
const label = dayjsObj.format(DATE_FORMAT); | ||
const hasDot = withDotDates.has(date); | ||
|
||
return ( | ||
<div | ||
className={dayCell} | ||
data-is-today={isToday} | ||
data-is-selected={isSelected} | ||
data-is-current-month={isCurrentMonth} | ||
aria-label={label} | ||
onClick={() => onSelect(date)} | ||
> | ||
{day} | ||
{hasDot && <div className={dot} />} | ||
</div> | ||
); | ||
}); |
45 changes: 45 additions & 0 deletions
45
packages/frontend/core/src/mobile/pages/workspace/detail/journal-date-picker/index.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
import { useCallback, useEffect, useState } from 'react'; | ||
|
||
import { JournalDatePickerContext } from './context'; | ||
import { ResizeViewport } from './viewport'; | ||
|
||
export interface JournalDatePickerProps { | ||
date: string; | ||
onChange: (date: string) => void; | ||
withDotDates: Set<string | null | undefined>; | ||
} | ||
export const JournalDatePicker = ({ | ||
date: selected, | ||
onChange, | ||
withDotDates, | ||
}: JournalDatePickerProps) => { | ||
const [cursor, setCursor] = useState(selected); | ||
|
||
// should update cursor when selected modified outside | ||
useEffect(() => { | ||
setCursor(selected); | ||
}, [selected]); | ||
|
||
const onSelect = useCallback( | ||
(date: string) => { | ||
setCursor(date); | ||
onChange(date); | ||
}, | ||
[onChange] | ||
); | ||
|
||
return ( | ||
<JournalDatePickerContext.Provider | ||
value={{ | ||
selected, | ||
onSelect, | ||
cursor, | ||
setCursor, | ||
width: window.innerWidth, | ||
withDotDates, | ||
}} | ||
> | ||
<ResizeViewport></ResizeViewport> | ||
</JournalDatePickerContext.Provider> | ||
); | ||
}; |
24 changes: 24 additions & 0 deletions
24
packages/frontend/core/src/mobile/pages/workspace/detail/journal-date-picker/month.css.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
import { style } from '@vanilla-extract/css'; | ||
|
||
export const monthViewClip = style({ | ||
width: '100%', | ||
height: '100%', | ||
overflow: 'hidden', | ||
}); | ||
|
||
export const monthsSwipe = style({ | ||
width: '300%', | ||
height: '100%', | ||
marginLeft: '-100%', | ||
display: 'flex', | ||
justifyContent: 'center', | ||
}); | ||
|
||
export const monthView = style({ | ||
width: 0, | ||
flex: 1, | ||
display: 'flex', | ||
flexDirection: 'column', | ||
gap: 4, | ||
padding: '0 16px', | ||
}); |
160 changes: 160 additions & 0 deletions
160
packages/frontend/core/src/mobile/pages/workspace/detail/journal-date-picker/month.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,160 @@ | ||
import anime from 'animejs'; | ||
import clsx from 'clsx'; | ||
import dayjs from 'dayjs'; | ||
import { | ||
useCallback, | ||
useContext, | ||
useEffect, | ||
useMemo, | ||
useRef, | ||
useState, | ||
} from 'react'; | ||
|
||
import { | ||
CELL_HEIGHT, | ||
DATE_FORMAT, | ||
HORIZONTAL_SWIPE_THRESHOLD, | ||
MONTH_VIEW_HEIGHT, | ||
ROWS_GAP, | ||
WEEK_VIEW_HEIGHT, | ||
} from './constants'; | ||
import { JournalDatePickerContext } from './context'; | ||
import * as styles from './month.css'; | ||
import { SwipeHelper } from './swipe-helper'; | ||
import { getFirstDayOfMonth } from './utils'; | ||
import { WeekRow } from './week'; | ||
export interface MonthViewProps { | ||
viewportHeight: number; | ||
} | ||
|
||
function getWeeks(date: string) { | ||
const today = dayjs(date); | ||
const firstDayOfMonth = today.startOf('month'); | ||
const firstWeekday = firstDayOfMonth.startOf('week'); | ||
|
||
const weeks = []; | ||
for (let i = 0; i < 6; i++) { | ||
const week = firstWeekday.add(i * 7, 'day'); | ||
weeks.push(week.format(DATE_FORMAT)); | ||
} | ||
|
||
return weeks; | ||
} | ||
|
||
export const MonthView = ({ viewportHeight }: MonthViewProps) => { | ||
const rootRef = useRef<HTMLDivElement>(null); | ||
const swipeRef = useRef<HTMLDivElement>(null); | ||
const { width, selected, onSelect, setCursor } = useContext( | ||
JournalDatePickerContext | ||
); | ||
const [swiping, setSwiping] = useState(false); | ||
const [animating, setAnimating] = useState(false); | ||
const [swipingDeltaX, setSwipingDeltaX] = useState(0); | ||
|
||
const weeks = useMemo( | ||
() => getWeeks(selected ?? dayjs().format(DATE_FORMAT)), | ||
[selected] | ||
); | ||
|
||
const firstWeekDayOfSelected = dayjs(selected) | ||
.startOf('week') | ||
.format(DATE_FORMAT); | ||
const activeRowIndex = weeks.indexOf(firstWeekDayOfSelected); | ||
|
||
// pointA: (WEEK_VIEW_HEIGHT, maxY) | ||
// pointB: (MONTH_VIEW_HEIGHT, 0) | ||
const maxY = -(activeRowIndex * (CELL_HEIGHT + ROWS_GAP)); | ||
const k = maxY / (WEEK_VIEW_HEIGHT - MONTH_VIEW_HEIGHT); | ||
const b = -k * MONTH_VIEW_HEIGHT; | ||
const translateY = k * viewportHeight + b; | ||
|
||
const translateX = Math.max(-width, Math.min(width, swipingDeltaX)); | ||
|
||
const animateTo = useCallback( | ||
(dir: 0 | 1 | -1) => { | ||
setAnimating(true); | ||
|
||
anime({ | ||
targets: swipeRef.current, | ||
translateX: -dir * width, | ||
duration: 300, | ||
easing: 'easeInOutSine', | ||
complete: () => { | ||
setSwipingDeltaX(0); | ||
setAnimating(false); | ||
// should recover swipe before change month | ||
if (dir !== 0) { | ||
setTimeout(() => onSelect(getFirstDayOfMonth(selected, dir))); | ||
} | ||
}, | ||
}); | ||
}, | ||
[onSelect, selected, width] | ||
); | ||
|
||
useEffect(() => { | ||
if (!rootRef.current) return; | ||
const swipeHelper = new SwipeHelper(); | ||
return swipeHelper.init(rootRef.current, { | ||
preventScroll: true, | ||
onSwipe: ({ deltaX }) => { | ||
setSwiping(true); | ||
setSwipingDeltaX(deltaX); | ||
if (Math.abs(deltaX) > HORIZONTAL_SWIPE_THRESHOLD) { | ||
setCursor(getFirstDayOfMonth(selected, deltaX > 0 ? -1 : 1)); | ||
} else { | ||
setCursor(selected); | ||
} | ||
}, | ||
onSwipeEnd: ({ deltaX }) => { | ||
setSwiping(false); | ||
if (Math.abs(deltaX) > HORIZONTAL_SWIPE_THRESHOLD) { | ||
animateTo(deltaX > 0 ? -1 : 1); | ||
} else { | ||
animateTo(0); | ||
} | ||
}, | ||
}); | ||
}, [animateTo, selected, setCursor]); | ||
|
||
return ( | ||
<div className={styles.monthViewClip} ref={rootRef}> | ||
<div | ||
ref={swipeRef} | ||
className={styles.monthsSwipe} | ||
style={{ transform: `translateX(${translateX}px)` }} | ||
> | ||
<MonthGrid | ||
hidden={!swiping && !animating} | ||
date={getFirstDayOfMonth(selected, -1)} | ||
/> | ||
{/* Active month */} | ||
<MonthGrid | ||
style={{ transform: `translateY(${translateY}px)` }} | ||
date={selected ?? ''} | ||
/> | ||
<MonthGrid | ||
hidden={!swiping && !animating} | ||
date={getFirstDayOfMonth(selected, 1)} | ||
/> | ||
</div> | ||
</div> | ||
); | ||
}; | ||
|
||
interface MonthGridProps extends React.HTMLAttributes<HTMLDivElement> { | ||
date: string; | ||
hidden?: boolean; | ||
} | ||
const MonthGrid = ({ date, className, hidden, ...props }: MonthGridProps) => { | ||
const weeks = useMemo( | ||
() => getWeeks(date ?? dayjs().format(DATE_FORMAT)), | ||
[date] | ||
); | ||
|
||
return ( | ||
<div className={clsx(styles.monthView, className)} {...props}> | ||
{hidden ? null : weeks.map(week => <WeekRow key={week} start={week} />)} | ||
</div> | ||
); | ||
}; |
Oops, something went wrong.