Remove dayjs and convert existing to Temporal

This commit is contained in:
Ben Grant 2023-05-28 18:00:28 +10:00
parent a74fee9318
commit d2bee83db4
15 changed files with 103 additions and 213 deletions

View file

@ -1,6 +1,7 @@
import { useEffect, useState } from 'react'
import { FieldValues, useController, UseControllerProps } from 'react-hook-form'
import { useTranslation } from 'react-i18next'
import { Temporal } from '@js-temporal/polyfill'
import { Description, Label, Wrapper } from '/src/components/Field/Field'
import ToggleField from '/src/components/ToggleField/ToggleField'
@ -27,7 +28,7 @@ const CalendarField = <TValues extends FieldValues>({
const [innerValue, setInnerValue] = useState({
specific: [],
week: [],
} satisfies Record<typeof type, string[]>)
} satisfies Record<typeof type, Temporal.PlainDate[]>)
useEffect(() => {
setInnerValue({ ...innerValue, [type]: field.value })

View file

@ -1,34 +1,29 @@
import { useCallback, useEffect, useRef, useState } from 'react'
import { useCallback, useMemo, useRef, useState } from 'react'
import { rotateArray } from '@giraugh/tools'
import { Dayjs } from 'dayjs'
import { Temporal } from '@js-temporal/polyfill'
import { ChevronLeft, ChevronRight } from 'lucide-react'
import Button from '/src/components/Button/Button'
import { useDayjs } from '/src/config/dayjs'
import { useTranslation } from '/src/i18n/client'
import { useStore } from '/src/stores'
import useSettingsStore from '/src/stores/settingsStore'
import { makeClass } from '/src/utils'
import { getWeekdayNames, makeClass } from '/src/utils'
import styles from './Month.module.scss'
interface MonthProps {
/** Array of dates in `DDMMYYYY` format */
/** Stringified PlainDate `YYYY-MM-DD` */
value: string[]
onChange: (value: string[]) => void
}
const Month = ({ value, onChange }: MonthProps) => {
const { t } = useTranslation('home')
const dayjs = useDayjs()
const { t, i18n } = useTranslation('home')
const weekStart = useStore(useSettingsStore, state => state.weekStart) ?? 0
const weekStart = useStore(useSettingsStore, state => state.weekStart) ?? 1
const [page, setPage] = useState({
month: dayjs().month(),
year: dayjs().year(),
})
const [dates, setDates] = useState(calculateMonth(dayjs().month(page.month).year(page.year), weekStart))
const [page, setPage] = useState<Temporal.PlainYearMonth>(Temporal.Now.plainDateISO().toPlainYearMonth())
const dates = useMemo(() => calculateMonth(page, weekStart), [page, weekStart])
// Ref and state required to rerender but also access static version in callbacks
const selectingRef = useRef<string[]>([])
@ -41,12 +36,6 @@ const Month = ({ value, onChange }: MonthProps) => {
const startPos = useRef({ x: 0, y: 0 })
const mode = useRef<'add' | 'remove'>()
// Update month view
useEffect(() => {
dayjs.updateLocale(dayjs.locale(), { weekStart })
setDates(calculateMonth(dayjs().month(page.month).year(page.year), weekStart))
}, [weekStart, page])
const handleFinishSelection = useCallback(() => {
if (mode.current === 'add') {
onChange([...value, ...selectingRef.current])
@ -60,31 +49,19 @@ const Month = ({ value, onChange }: MonthProps) => {
<div className={styles.header}>
<Button
title={t<string>('form.dates.tooltips.previous')}
onClick={() => {
if (page.month - 1 < 0) {
setPage({ month: 11, year: page.year - 1 })
} else {
setPage({ ...page, month: page.month - 1 })
}
}}
onClick={() => setPage(page.subtract({ months: 1 }))}
icon={<ChevronLeft />}
/>
<span>{dayjs.months()[page.month]} {page.year}</span>
<span>{page.toPlainDate({ day: 1 }).toLocaleString(i18n.language, { month: 'long', year: 'numeric' })}</span>
<Button
title={t<string>('form.dates.tooltips.next')}
onClick={() => {
if (page.month + 1 > 11) {
setPage({ month: 0, year: page.year + 1 })
} else {
setPage({ ...page, month: page.month + 1 })
}
}}
onClick={() => setPage(page.add({ months: 1 }))}
icon={<ChevronRight />}
/>
</div>
<div className={styles.dayLabels}>
{(rotateArray(dayjs.weekdaysShort(), -weekStart)).map(name =>
{(rotateArray(getWeekdayNames(i18n.language, 'short'), weekStart)).map(name =>
<label key={name}>{name}</label>
)}
</div>
@ -96,27 +73,27 @@ const Month = ({ value, onChange }: MonthProps) => {
className={makeClass(
styles.date,
date.month !== page.month && styles.otherMonth,
date.isToday && styles.today,
date.equals(Temporal.Now.plainDateISO()) && styles.today,
(
(!(mode.current === 'remove' && selecting.includes(date.str)) && value.includes(date.str))
|| (mode.current === 'add' && selecting.includes(date.str))
(!(mode.current === 'remove' && selecting.includes(date.toString())) && value.includes(date.toString()))
|| (mode.current === 'add' && selecting.includes(date.toString()))
) && styles.selected,
)}
key={date.str}
title={`${date.day} ${dayjs.months()[date.month]}${date.isToday ? ` (${t('form.dates.tooltips.today')})` : ''}`}
key={date.toString()}
title={`${date.toLocaleString(i18n.language, { day: 'numeric', month: 'long' })}${date.equals(Temporal.Now.plainDateISO()) ? ` (${t('form.dates.tooltips.today')})` : ''}`}
onKeyDown={e => {
if (e.key === ' ' || e.key === 'Enter') {
if (value.includes(date.str)) {
onChange(value.filter(d => d !== date.str))
if (value.includes(date.toString())) {
onChange(value.filter(d => d !== date.toString()))
} else {
onChange([...value, date.str])
onChange([...value, date.toString()])
}
}
}}
onPointerDown={e => {
startPos.current = { x, y }
mode.current = value.includes(date.str) ? 'remove' : 'add'
setSelecting([date.str])
mode.current = value.includes(date.toString()) ? 'remove' : 'add'
setSelecting([date.toString()])
e.currentTarget.releasePointerCapture(e.pointerId)
document.addEventListener('pointerup', handleFinishSelection, { once: true })
@ -129,10 +106,10 @@ const Month = ({ value, onChange }: MonthProps) => {
found.push({ y: cy, x: cx })
}
}
setSelecting(found.map(d => dates[d.y][d.x].str))
setSelecting(found.map(d => dates[d.y][d.x].toString()))
}
}}
>{date.day}</button>)
>{date.toLocaleString(i18n.language, { day: 'numeric' })}</button>)
)}
</div>
</>
@ -140,32 +117,19 @@ const Month = ({ value, onChange }: MonthProps) => {
export default Month
interface Date {
str: string
day: number
month: number
isToday: boolean
}
/** Calculate the dates to show for the month in a 2d array */
const calculateMonth = (date: Dayjs, weekStart: 0 | 1) => {
const daysInMonth = date.daysInMonth()
const daysBefore = date.date(1).day() - weekStart
const daysAfter = 6 - date.date(daysInMonth).day() + weekStart
const calculateMonth = (month: Temporal.PlainYearMonth, weekStart: 0 | 1) => {
const daysBefore = month.toPlainDate({ day: 1 }).dayOfWeek - (weekStart ? 0 : 1)
const daysAfter = 6 - month.toPlainDate({ day: month.daysInMonth }).dayOfWeek + (weekStart ? 0 : 1)
const dates: Date[][] = []
let curDate = date.date(1).subtract(daysBefore, 'day')
const dates: Temporal.PlainDate[][] = []
let curDate = month.toPlainDate({ day: 1 }).subtract({ days: daysBefore })
let y = 0
let x = 0
for (let i = 0; i < daysBefore + daysInMonth + daysAfter; i++) {
for (let i = 0; i < daysBefore + month.daysInMonth + daysAfter; i++) {
if (x === 0) dates[y] = []
dates[y][x] = {
str: curDate.format('DDMMYYYY'),
day: curDate.date(),
month: curDate.month(),
isToday: curDate.isToday(),
}
curDate = curDate.add(1, 'day')
dates[y][x] = curDate
curDate = curDate.add({ days: 1 })
x++
if (x > 6) {
x = 0

View file

@ -1,7 +1,7 @@
import { useCallback, useMemo, useRef, useState } from 'react'
import { rotateArray } from '@giraugh/tools'
import { range, rotateArray } from '@giraugh/tools'
import { Temporal } from '@js-temporal/polyfill'
import { useDayjs } from '/src/config/dayjs'
import { useTranslation } from '/src/i18n/client'
import { useStore } from '/src/stores'
import useSettingsStore from '/src/stores/settingsStore'
@ -11,22 +11,17 @@ import { makeClass } from '/src/utils'
import styles from '../Month/Month.module.scss'
interface WeekdaysProps {
/** Array of weekdays as numbers from 0-6 (as strings) */
/** dayOfWeek 1-7 as a string */
value: string[]
onChange: (value: string[]) => void
}
const Weekdays = ({ value, onChange }: WeekdaysProps) => {
const { t } = useTranslation('home')
const dayjs = useDayjs()
const { t, i18n } = useTranslation('home')
const weekStart = useStore(useSettingsStore, state => state.weekStart) ?? 0
const weekStart = useStore(useSettingsStore, state => state.weekStart) ?? 1
const weekdays = useMemo(() => rotateArray(dayjs.weekdaysShort().map((name, i) => ({
name,
isToday: dayjs().day() === i,
str: String(i),
})), -weekStart), [weekStart])
const weekdays = useMemo(() => rotateArray(range(1, 7).map(i => Temporal.Now.plainDateISO().add({ days: i - Temporal.Now.plainDateISO().dayOfWeek })), weekStart), [weekStart])
// Ref and state required to rerender but also access static version in callbacks
const selectingRef = useRef<string[]>([])
@ -54,27 +49,27 @@ const Weekdays = ({ value, onChange }: WeekdaysProps) => {
type="button"
className={makeClass(
styles.date,
day.isToday && styles.today,
day.equals(Temporal.Now.plainDateISO()) && styles.today,
(
(!(mode.current === 'remove' && selecting.includes(day.str)) && value.includes(day.str))
|| (mode.current === 'add' && selecting.includes(day.str))
(!(mode.current === 'remove' && selecting.includes(day.dayOfWeek.toString())) && value.includes(day.dayOfWeek.toString()))
|| (mode.current === 'add' && selecting.includes(day.dayOfWeek.toString()))
) && styles.selected,
)}
key={day.name}
title={day.isToday ? t<string>('form.dates.tooltips.today') : undefined}
key={day.toString()}
title={day.equals(Temporal.Now.plainDateISO()) ? t<string>('form.dates.tooltips.today') : undefined}
onKeyDown={e => {
if (e.key === ' ' || e.key === 'Enter') {
if (value.includes(day.str)) {
onChange(value.filter(d => d !== day.str))
if (value.includes(day.dayOfWeek.toString())) {
onChange(value.filter(d => d !== day.dayOfWeek.toString()))
} else {
onChange([...value, day.str])
onChange([...value, day.dayOfWeek.toString()])
}
}
}}
onPointerDown={e => {
startPos.current = i
mode.current = value.includes(day.str) ? 'remove' : 'add'
setSelecting([day.str])
mode.current = value.includes(day.dayOfWeek.toString()) ? 'remove' : 'add'
setSelecting([day.dayOfWeek.toString()])
e.currentTarget.releasePointerCapture(e.pointerId)
document.addEventListener('pointerup', handleFinishSelection, { once: true })
@ -83,12 +78,12 @@ const Weekdays = ({ value, onChange }: WeekdaysProps) => {
if (mode.current) {
const found = []
for (let ci = Math.min(startPos.current, i); ci < Math.max(startPos.current, i) + 1; ci++) {
found.push(weekdays[ci].str)
found.push(weekdays[ci].dayOfWeek.toString())
}
setSelecting(found)
}
}}
>{day.name}</button>
>{day.toLocaleString(i18n.language, { weekday: 'short' })}</button>
)}
</div>
}

View file

@ -3,6 +3,7 @@
import { useState } from 'react'
import { SubmitHandler, useForm } from 'react-hook-form'
import { useRouter } from 'next/navigation'
import { Temporal } from '@js-temporal/polyfill'
import Button from '/src/components/Button/Button'
import CalendarField from '/src/components/CalendarField/CalendarField'
@ -11,7 +12,6 @@ import SelectField from '/src/components/SelectField/SelectField'
import TextField from '/src/components/TextField/TextField'
import TimeRangeField from '/src/components/TimeRangeField/TimeRangeField'
import { API_BASE } from '/src/config/api'
import { useDayjs } from '/src/config/dayjs'
import { useTranslation } from '/src/i18n/client'
import timezones from '/src/res/timezones.json'
@ -36,7 +36,6 @@ const defaultValues: Fields = {
const CreateForm = () => {
const { t } = useTranslation('home')
const dayjs = useDayjs()
const { push } = useRouter()
const {
@ -58,38 +57,38 @@ const CreateForm = () => {
if (dates.length === 0) {
return setError(t('form.errors.no_dates'))
}
const isSpecificDates = dates[0].length === 8
if (time.start === time.end) {
return setError(t('form.errors.same_times'))
}
const times = dates.reduce((times, date) => {
// If format is `YYYY-MM-DD` or `d`
const isSpecificDates = dates[0].length !== 1
const times = dates.reduce((times, dateStr) => {
const day = []
const date = isSpecificDates
? Temporal.PlainDate.from(dateStr)
: Temporal.Now.plainDateISO().add({ days: Number(dateStr) - Temporal.Now.plainDateISO().dayOfWeek })
for (let i = time.start; i < (time.start > time.end ? 24 : time.end); i++) {
const dateTime = date.toZonedDateTime({ timeZone: timezone, plainTime: Temporal.PlainTime.from({ hour: i }) }).withTimeZone('UTC')
if (isSpecificDates) {
day.push(
dayjs.tz(date, 'DDMMYYYY', timezone)
.hour(i).minute(0).utc().format('HHmm-DDMMYYYY')
)
// Format as `HHmm-DDMMYYYY`
day.push(`${dateTime.hour.toString().padStart(2, '0')}${dateTime.minute.toString().padStart(2, '0')}-${dateTime.day.toString().padStart(2, '0')}${dateTime.month.toString().padStart(2, '0')}${dateTime.year.toString().padStart(4, '0')}`)
} else {
day.push(
dayjs().tz(timezone)
.day(Number(date)).hour(i).minute(0).utc().format('HHmm-d')
)
// Format as `HHmm-d`
day.push(`${dateTime.hour.toString().padStart(2, '0')}${dateTime.minute.toString().padStart(2, '0')}-${String(dateTime.dayOfWeek === 7 ? 0 : dateTime.dayOfWeek)}`)
}
}
if (time.start > time.end) {
for (let i = 0; i < time.end; i++) {
const dateTime = date.toZonedDateTime({ timeZone: timezone, plainTime: Temporal.PlainTime.from({ hour: i }) }).withTimeZone('UTC')
if (isSpecificDates) {
day.push(
dayjs.tz(date, 'DDMMYYYY', timezone)
.hour(i).minute(0).utc().format('HHmm-DDMMYYYY')
)
// Format as `HHmm-DDMMYYYY`
day.push(`${dateTime.hour.toString().padStart(2, '0')}${dateTime.minute.toString().padStart(2, '0')}-${dateTime.day.toString().padStart(2, '0')}${dateTime.month.toString().padStart(2, '0')}${dateTime.year.toString().padStart(4, '0')}`)
} else {
day.push(
dayjs().tz(timezone)
.day(Number(date)).hour(i).minute(0).utc().format('HHmm-d')
)
// Format as `HHmm-d`
day.push(`${dateTime.hour.toString().padStart(2, '0')}${dateTime.minute.toString().padStart(2, '0')}-${String(dateTime.dayOfWeek === 7 ? 0 : dateTime.dayOfWeek)}`)
}
}
}

View file

@ -1,13 +1,14 @@
'use client'
import Link from 'next/link'
import { Temporal } from '@js-temporal/polyfill'
import Content from '/src/components/Content/Content'
import Section from '/src/components/Section/Section'
import { useDayjs } from '/src/config/dayjs'
import { useTranslation } from '/src/i18n/client'
import { useStore } from '/src/stores'
import useRecentsStore from '/src/stores/recentsStore'
import { relativeTimeFormat } from '/src/utils'
import styles from './Recents.module.scss'
@ -17,8 +18,7 @@ interface RecentsProps {
const Recents = ({ target }: RecentsProps) => {
const recents = useStore(useRecentsStore, state => state.recents)
const { t } = useTranslation(['home', 'common'])
const dayjs = useDayjs()
const { t, i18n } = useTranslation(['home', 'common'])
return recents?.length ? <Section id="recents">
<Content>
@ -28,8 +28,8 @@ const Recents = ({ target }: RecentsProps) => {
<span className={styles.name}>{event.name}</span>
<span
className={styles.date}
title={dayjs.unix(event.created_at).format('D MMMM, YYYY')}
>{t('common:created', { date: dayjs.unix(event.created_at).fromNow() })}</span>
title={Temporal.Instant.fromEpochSeconds(event.created_at).toLocaleString(i18n.language, { dateStyle: 'long' })}
>{t('common:created', { date: relativeTimeFormat(Temporal.Instant.fromEpochSeconds(event.created_at), i18n.language) })}</span>
</Link>
))}
</Content>

View file

@ -3,6 +3,7 @@
import { useCallback, useEffect, useRef, useState } from 'react'
import { useRouter } from 'next/navigation'
import { maps } from 'hue-map'
import { MapKey } from 'hue-map/dist/maps'
import { Settings as SettingsIcon } from 'lucide-react'
import SelectField from '/src/components/SelectField/SelectField'
@ -64,8 +65,8 @@ const Settings = () => {
'Sunday': t('options.weekStart.options.Sunday'),
'Monday': t('options.weekStart.options.Monday'),
}}
value={store?.weekStart === 0 ? 'Sunday' : 'Monday'}
onChange={value => store?.setWeekStart(value === 'Sunday' ? 0 : 1)}
value={store?.weekStart === 1 ? 'Sunday' : 'Monday'}
onChange={value => store?.setWeekStart(value === 'Sunday' ? 1 : 0)}
/>
<ToggleField
@ -103,7 +104,7 @@ const Settings = () => {
}}
isSmall
value={store?.colormap}
onChange={event => store?.setColormap(event.target.value)}
onChange={event => store?.setColormap(event.target.value as MapKey)}
/>
<ToggleField

View file

@ -2,9 +2,10 @@
import { useRef } from 'react'
import { FieldValues, useController, UseControllerProps } from 'react-hook-form'
import dayjs from 'dayjs'
import { Temporal } from '@js-temporal/polyfill'
import { Description, Label, Wrapper } from '/src/components/Field/Field'
import { useTranslation } from '/src/i18n/client'
import { useStore } from '/src/stores'
import useSettingsStore from '/src/stores/settingsStore'
@ -81,6 +82,7 @@ interface HandleProps {
const Handle = ({ value, onChange, labelPadding }: HandleProps) => {
const timeFormat = useStore(useSettingsStore, state => state.timeFormat)
const { i18n } = useTranslation()
const isMoving = useRef(false)
const rangeRect = useRef({ left: 0, width: 0 })
@ -106,7 +108,7 @@ const Handle = ({ value, onChange, labelPadding }: HandleProps) => {
left: `calc(${value * 4.166}% - 11px)`,
'--extra-padding': labelPadding,
} as React.CSSProperties}
data-label={timeFormat === '24h' ? times[value] : dayjs().hour(Number(times[value])).format('ha')}
data-label={Temporal.PlainTime.from({ hour: Number(times[value] === '24' ? '00' : times[value]) }).toLocaleString(i18n.language, { hour: 'numeric', hour12: timeFormat === '12h' })}
onMouseDown={() => {
document.addEventListener('mousemove', handleMouseMove)
isMoving.current = true