@sanity/rich-date-input 2.0.10 → 3.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +1 -1
- package/README.md +67 -33
- package/dist/index.d.ts +36 -0
- package/dist/index.esm.js +258 -0
- package/dist/index.esm.js.map +1 -0
- package/dist/index.js +263 -0
- package/dist/index.js.map +1 -0
- package/package.json +79 -24
- package/sanity.json +2 -15
- package/src/components/RelativeDateTimePicker.tsx +56 -0
- package/src/components/RichDateInput.tsx +58 -0
- package/src/components/TimezoneButton.tsx +31 -0
- package/src/components/TimezoneSelector.tsx +82 -0
- package/src/index.ts +12 -0
- package/src/schema.ts +61 -0
- package/src/types/index.ts +18 -0
- package/src/utils/index.ts +40 -0
- package/v2-incompatible.js +11 -0
- package/schema.js +0 -1
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import {SearchIcon} from '@sanity/icons'
|
|
2
|
+
import {ObjectInputProps, set} from 'sanity'
|
|
3
|
+
import {allTimezones, unlocalizeDateTime} from '../utils'
|
|
4
|
+
import {NormalizedTimeZone, RichDate} from '../types'
|
|
5
|
+
import {Autocomplete, Card, Text, Box} from '@sanity/ui'
|
|
6
|
+
import {formatInTimeZone, zonedTimeToUtc} from 'date-fns-tz'
|
|
7
|
+
|
|
8
|
+
interface TimezoneSelectorProps {
|
|
9
|
+
onChange: Pick<ObjectInputProps, 'onChange'>['onChange']
|
|
10
|
+
value?: RichDate
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const TimezoneSelector = (props: TimezoneSelectorProps) => {
|
|
14
|
+
const {onChange, value} = props
|
|
15
|
+
const currentTz = allTimezones.find((tz) => tz.name === value?.timezone)
|
|
16
|
+
const userTzName = Intl.DateTimeFormat().resolvedOptions().timeZone
|
|
17
|
+
const userTz = (allTimezones.find((tz) => tz.name === userTzName) ??
|
|
18
|
+
allTimezones.find((tz) => tz.group.includes(userTzName)))!
|
|
19
|
+
|
|
20
|
+
const handleTimezoneChange = (selectedTz: string) => {
|
|
21
|
+
const newTimezone =
|
|
22
|
+
allTimezones.find((tz) => tz.value === selectedTz) ?? (userTz as NormalizedTimeZone)
|
|
23
|
+
|
|
24
|
+
const offset = newTimezone.currentTimeOffsetInMinutes ?? 0
|
|
25
|
+
const timezonePatch = set(newTimezone.name, ['timezone'])
|
|
26
|
+
const offsetPatch = set(offset, ['offset'])
|
|
27
|
+
const patches = [timezonePatch, offsetPatch]
|
|
28
|
+
|
|
29
|
+
//then, recalculate UTC and local from "old" time with the new offset
|
|
30
|
+
if (value?.utc) {
|
|
31
|
+
const desiredDateTime = unlocalizeDateTime(value.utc, value.timezone)
|
|
32
|
+
const newUtcDate = zonedTimeToUtc(desiredDateTime, newTimezone.name).toISOString()
|
|
33
|
+
const newLocalDate = formatInTimeZone(
|
|
34
|
+
newUtcDate,
|
|
35
|
+
newTimezone.name,
|
|
36
|
+
"yyyy-MM-dd'T'HH:mm:ssXXX",
|
|
37
|
+
)
|
|
38
|
+
patches.push(set(newUtcDate, ['utc']))
|
|
39
|
+
patches.push(set(newLocalDate, ['local']))
|
|
40
|
+
}
|
|
41
|
+
onChange(patches)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return (
|
|
45
|
+
//taken from Scheduled Publishing, again!
|
|
46
|
+
//https://github.com/sanity-io/sanity-plugin-scheduled-publishing/blob/bb282e3df9a8a73df37fab8ee1fdd0e2430745be/src/components/dialogs/DialogTimeZone.tsx#L100
|
|
47
|
+
<Box padding={4}>
|
|
48
|
+
<Autocomplete
|
|
49
|
+
fontSize={2}
|
|
50
|
+
icon={SearchIcon}
|
|
51
|
+
id="timezone"
|
|
52
|
+
onChange={handleTimezoneChange}
|
|
53
|
+
openButton
|
|
54
|
+
options={allTimezones}
|
|
55
|
+
padding={4}
|
|
56
|
+
placeholder="Search for a city or time zone"
|
|
57
|
+
popover={{
|
|
58
|
+
boundaryElement: document.querySelector('body'),
|
|
59
|
+
constrainSize: true,
|
|
60
|
+
placement: 'bottom-start',
|
|
61
|
+
}}
|
|
62
|
+
renderOption={(option) => {
|
|
63
|
+
return (
|
|
64
|
+
<Card as="button" padding={3}>
|
|
65
|
+
<Text size={1} textOverflow="ellipsis">
|
|
66
|
+
<span>GMT{option.offset}</span>
|
|
67
|
+
<span style={{fontWeight: 500, marginLeft: '1em'}}>{option.alternativeName}</span>
|
|
68
|
+
<span style={{marginLeft: '1em'}}>{option.mainCities}</span>
|
|
69
|
+
</Text>
|
|
70
|
+
</Card>
|
|
71
|
+
)
|
|
72
|
+
}}
|
|
73
|
+
renderValue={(_, option) => {
|
|
74
|
+
if (!option) return ''
|
|
75
|
+
return `${option.alternativeName} (${option.namePretty})`
|
|
76
|
+
}}
|
|
77
|
+
tabIndex={-1}
|
|
78
|
+
value={currentTz?.value ?? userTz.value}
|
|
79
|
+
/>
|
|
80
|
+
</Box>
|
|
81
|
+
)
|
|
82
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import {definePlugin} from 'sanity'
|
|
2
|
+
import {richDateSchema, RichDateDefinition, RichDateSchemaType} from './schema'
|
|
3
|
+
import {RichDate} from './types'
|
|
4
|
+
|
|
5
|
+
export const richDate = definePlugin({
|
|
6
|
+
name: 'v3-rich-date-input',
|
|
7
|
+
schema: {
|
|
8
|
+
types: [richDateSchema],
|
|
9
|
+
},
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
export type {RichDateDefinition, RichDateSchemaType, RichDate}
|
package/src/schema.ts
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DatetimeDefinition,
|
|
3
|
+
ObjectDefinition,
|
|
4
|
+
ObjectSchemaType,
|
|
5
|
+
defineField,
|
|
6
|
+
defineType,
|
|
7
|
+
} from 'sanity'
|
|
8
|
+
import {RichDateInput} from './components/RichDateInput'
|
|
9
|
+
|
|
10
|
+
const richDateTypeName = 'richDate' as const
|
|
11
|
+
|
|
12
|
+
export type RichDateSchemaType = Omit<ObjectSchemaType, 'options'> & {
|
|
13
|
+
options?: DatetimeDefinition['options']
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* @public
|
|
18
|
+
*/
|
|
19
|
+
export interface RichDateDefinition extends Omit<ObjectDefinition, 'type' | 'fields' | 'options'> {
|
|
20
|
+
type: typeof richDateTypeName
|
|
21
|
+
options?: DatetimeDefinition['options']
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
declare module 'sanity' {
|
|
25
|
+
//allows the custom input to be valid for the schema def
|
|
26
|
+
export interface IntrinsicDefinitions {
|
|
27
|
+
richDate: RichDateDefinition
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export const richDateSchema = defineType({
|
|
32
|
+
name: richDateTypeName,
|
|
33
|
+
title: 'Rich Date',
|
|
34
|
+
type: 'object',
|
|
35
|
+
fields: [
|
|
36
|
+
defineField({
|
|
37
|
+
name: 'local',
|
|
38
|
+
title: 'Local',
|
|
39
|
+
type: 'string',
|
|
40
|
+
}),
|
|
41
|
+
defineField({
|
|
42
|
+
name: 'utc',
|
|
43
|
+
title: 'UTC',
|
|
44
|
+
type: 'string',
|
|
45
|
+
}),
|
|
46
|
+
defineField({
|
|
47
|
+
name: 'timezone',
|
|
48
|
+
title: 'Timezone',
|
|
49
|
+
type: 'string',
|
|
50
|
+
}),
|
|
51
|
+
defineField({
|
|
52
|
+
name: 'offset',
|
|
53
|
+
title: 'Offset',
|
|
54
|
+
type: 'number',
|
|
55
|
+
}),
|
|
56
|
+
],
|
|
57
|
+
|
|
58
|
+
components: {
|
|
59
|
+
input: RichDateInput,
|
|
60
|
+
},
|
|
61
|
+
})
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export interface RichDate {
|
|
2
|
+
local: string
|
|
3
|
+
utc: string
|
|
4
|
+
timezone: string
|
|
5
|
+
offset: number
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface NormalizedTimeZone {
|
|
9
|
+
abbreviation: string
|
|
10
|
+
alternativeName: string
|
|
11
|
+
mainCities: string
|
|
12
|
+
name: string
|
|
13
|
+
namePretty: string
|
|
14
|
+
offset: string
|
|
15
|
+
value: string
|
|
16
|
+
currentTimeOffsetInMinutes: number
|
|
17
|
+
group: string[]
|
|
18
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import {getTimeZones} from '@vvo/tzdb'
|
|
2
|
+
import {NormalizedTimeZone} from '../types'
|
|
3
|
+
import {formatInTimeZone} from 'date-fns-tz'
|
|
4
|
+
|
|
5
|
+
export const unlocalizeDateTime = (datetime: string, timezone: string): string => {
|
|
6
|
+
return formatInTimeZone(datetime, timezone, 'yyyy-MM-dd HH:mm:ss')
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/* We have to "fake" a UTC date to make the datepicker look "right"
|
|
10
|
+
* to the user. For example, if someone sets 7:00AM PST, which is 3PM UTC
|
|
11
|
+
* and I am on the east coast, I want to have 12:00PM UTC, which will look like 7:00AM to me
|
|
12
|
+
* In other words, UTC minus 3 hours, or (UTC(my offset - their offset))
|
|
13
|
+
* this is purely cosmetic and should not be saved at all
|
|
14
|
+
*/
|
|
15
|
+
export const getConstructedUTCDate = (utc: string, offset: number): string => {
|
|
16
|
+
const date = new Date(utc)
|
|
17
|
+
const currentOffset = new Date().getTimezoneOffset() * -1
|
|
18
|
+
const diff = currentOffset - offset
|
|
19
|
+
const fakeUTCDate = new Date(date.getTime() - diff * 60 * 1000)
|
|
20
|
+
return fakeUTCDate.toISOString()
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
//keep some consistency with scheduled publishing
|
|
24
|
+
//https://github.com/sanity-io/sanity-plugin-scheduled-publishing/blob/bb282e3df9a8a73df37fab8ee1fdd0e2430745be/src/hooks/useTimeZone.tsx#L17
|
|
25
|
+
export const allTimezones = getTimeZones().map((tz) => {
|
|
26
|
+
return {
|
|
27
|
+
abbreviation: tz.abbreviation,
|
|
28
|
+
alternativeName: tz.alternativeName,
|
|
29
|
+
mainCities: tz.mainCities.join(', '),
|
|
30
|
+
// Main time zone name 'Africa/Dar_es_Salaam'
|
|
31
|
+
name: tz.name,
|
|
32
|
+
// Time zone name with underscores removed
|
|
33
|
+
namePretty: tz.name.replaceAll('_', ' '),
|
|
34
|
+
offset: tz.currentTimeFormat.split(' ')[0],
|
|
35
|
+
// all searchable text - this is transformed before being rendered in `<AutoComplete>`
|
|
36
|
+
value: `${tz.currentTimeFormat} ${tz.abbreviation} ${tz.name}`,
|
|
37
|
+
currentTimeOffsetInMinutes: tz.currentTimeOffsetInMinutes,
|
|
38
|
+
group: tz.group,
|
|
39
|
+
} as NormalizedTimeZone
|
|
40
|
+
})
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
const {showIncompatiblePluginDialog} = require('@sanity/incompatible-plugin')
|
|
2
|
+
const {name, version, sanityExchangeUrl} = require('./package.json')
|
|
3
|
+
|
|
4
|
+
export default showIncompatiblePluginDialog({
|
|
5
|
+
name: name,
|
|
6
|
+
versions: {
|
|
7
|
+
v3: version,
|
|
8
|
+
v2: undefined,
|
|
9
|
+
},
|
|
10
|
+
sanityExchangeUrl,
|
|
11
|
+
})
|
package/schema.js
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
module.exports = require('./lib/schema')
|