├── .editorconfig ├── .eslintignore ├── .eslintrc ├── .github └── workflows │ └── main.yml ├── .gitignore ├── .husky ├── commit-msg └── pre-commit ├── .npmignore ├── .prettierignore ├── .prettierrc ├── .releaserc.json ├── CHANGELOG.md ├── LICENSE ├── README.md ├── assets └── plugin.gif ├── commitlint.config.js ├── lint-staged.config.js ├── package-lock.json ├── package.config.ts ├── package.json ├── renovate.json ├── sanity.json ├── src ├── components │ ├── RelativeDateTimePicker.tsx │ ├── RichDateInput.tsx │ ├── TimezoneButton.tsx │ └── TimezoneSelector.tsx ├── index.ts ├── schema.ts ├── types │ └── index.ts └── utils │ └── index.ts ├── tsconfig.dist.json ├── tsconfig.json ├── tsconfig.settings.json └── v2-incompatible.js /.editorconfig: -------------------------------------------------------------------------------- 1 | ; editorconfig.org 2 | root = true 3 | charset= utf8 4 | 5 | [*] 6 | end_of_line = lf 7 | insert_final_newline = true 8 | trim_trailing_whitespace = true 9 | indent_style = space 10 | indent_size = 2 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | *.js 2 | .eslintrc.js 3 | commitlint.config.js 4 | dist 5 | lint-staged.config.js 6 | package.config.ts 7 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "env": { 4 | "node": true, 5 | "browser": true 6 | }, 7 | "extends": [ 8 | "sanity", 9 | "sanity/typescript", 10 | "sanity/react", 11 | "plugin:react-hooks/recommended", 12 | "plugin:prettier/recommended", 13 | "plugin:react/jsx-runtime" 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: CI & Release 3 | 4 | # Workflow name based on selected inputs. Fallback to default Github naming when expression evaluates to empty string 5 | run-name: >- 6 | ${{ 7 | inputs.release && inputs.test && format('Build {0} ➤ Test ➤ Publish to NPM', github.ref_name) || 8 | inputs.release && !inputs.test && format('Build {0} ➤ Skip Tests ➤ Publish to NPM', github.ref_name) || 9 | github.event_name == 'workflow_dispatch' && inputs.test && format('Build {0} ➤ Test', github.ref_name) || 10 | github.event_name == 'workflow_dispatch' && !inputs.test && format('Build {0} ➤ Skip Tests', github.ref_name) || 11 | '' 12 | }} 13 | 14 | on: 15 | # Build on pushes branches that have a PR (including drafts) 16 | pull_request: 17 | # Build on commits pushed to branches without a PR if it's in the allowlist 18 | push: 19 | branches: [main] 20 | # https://docs.github.com/en/actions/managing-workflow-runs/manually-running-a-workflow 21 | workflow_dispatch: 22 | inputs: 23 | test: 24 | description: Run tests 25 | required: true 26 | default: true 27 | type: boolean 28 | release: 29 | description: Release new version 30 | required: true 31 | default: false 32 | type: boolean 33 | 34 | concurrency: 35 | # On PRs builds will cancel if new pushes happen before the CI completes, as it defines `github.head_ref` and gives it the name of the branch the PR wants to merge into 36 | # Otherwise `github.run_id` ensures that you can quickly merge a queue of PRs without causing tests to auto cancel on any of the commits pushed to main. 37 | group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} 38 | cancel-in-progress: true 39 | 40 | permissions: 41 | contents: read # for checkout 42 | 43 | jobs: 44 | build: 45 | runs-on: ubuntu-latest 46 | name: Lint & Build 47 | steps: 48 | - uses: actions/checkout@v3 49 | - uses: actions/setup-node@v3 50 | with: 51 | cache: npm 52 | node-version: lts/* 53 | - run: npm clean-install 54 | # Linting can be skipped 55 | - run: npm run lint --if-present 56 | if: github.event.inputs.test != 'false' 57 | # But not the build script, as semantic-release will crash if this command fails so it makes sense to test it early 58 | - run: npm run prepublishOnly --if-present 59 | 60 | test: 61 | needs: build 62 | # The test matrix can be skipped, in case a new release needs to be fast-tracked and tests are already passing on main 63 | if: github.event.inputs.test != 'false' 64 | runs-on: ${{ matrix.os }} 65 | name: Node.js ${{ matrix.node }} / ${{ matrix.os }} 66 | strategy: 67 | # A test failing on windows doesn't mean it'll fail on macos. It's useful to let all tests run to its completion to get the full picture 68 | fail-fast: false 69 | matrix: 70 | # Run the testing suite on each major OS with the latest LTS release of Node.js 71 | os: [macos-latest, ubuntu-latest, windows-latest] 72 | node: [lts/*] 73 | # It makes sense to also test the oldest, and latest, versions of Node.js, on ubuntu-only since it's the fastest CI runner 74 | include: 75 | - os: ubuntu-latest 76 | # Test the oldest LTS release of Node that's still receiving bugfixes and security patches, versions older than that have reached End-of-Life 77 | node: lts/-1 78 | - os: ubuntu-latest 79 | # Test the actively developed version that will become the latest LTS release next October 80 | node: current 81 | steps: 82 | # It's only necessary to do this for windows, as mac and ubuntu are sane OS's that already use LF 83 | - name: Set git to use LF 84 | if: matrix.os == 'windows-latest' 85 | run: | 86 | git config --global core.autocrlf false 87 | git config --global core.eol lf 88 | - uses: actions/checkout@v3 89 | - uses: actions/setup-node@v3 90 | with: 91 | cache: npm 92 | node-version: ${{ matrix.node }} 93 | - run: npm install 94 | - run: npm test --if-present 95 | 96 | release: 97 | permissions: 98 | contents: write # to be able to publish a GitHub release 99 | issues: write # to be able to comment on released issues 100 | pull-requests: write # to be able to comment on released pull requests 101 | id-token: write # to enable use of OIDC for npm provenance 102 | needs: [build, test] 103 | # only run if opt-in during workflow_dispatch 104 | if: always() && github.event.inputs.release == 'true' && needs.build.result != 'failure' && needs.test.result != 'failure' && needs.test.result != 'cancelled' 105 | runs-on: ubuntu-latest 106 | name: Semantic release 107 | steps: 108 | - uses: actions/checkout@v3 109 | with: 110 | # Need to fetch entire commit history to 111 | # analyze every commit since last release 112 | fetch-depth: 0 113 | - uses: actions/setup-node@v3 114 | with: 115 | cache: npm 116 | node-version: lts/* 117 | - run: npm clean-install 118 | - run: npm audit signatures 119 | # Branches that will release new versions are defined in .releaserc.json 120 | - run: npx semantic-release 121 | # Don't allow interrupting the release step if the job is cancelled, as it can lead to an inconsistent state 122 | # e.g. git tags were pushed but it exited before `npm publish` 123 | if: always() 124 | env: 125 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 126 | NPM_TOKEN: ${{ secrets.NPM_PUBLISH_TOKEN }} 127 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # nyc test coverage 18 | .nyc_output 19 | 20 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 21 | .grunt 22 | 23 | # node-waf configuration 24 | .lock-wscript 25 | 26 | # Compiled binary addons (http://nodejs.org/api/addons.html) 27 | build/Release 28 | 29 | # Dependency directories 30 | node_modules 31 | jspm_packages 32 | 33 | # Optional npm cache directory 34 | .npm 35 | 36 | # Optional REPL history 37 | .node_repl_history 38 | 39 | # macOS finder cache file 40 | .DS_Store 41 | 42 | # VS Code settings 43 | .vscode 44 | 45 | # IntelliJ 46 | .idea 47 | *.iml 48 | 49 | # Cache 50 | .cache 51 | 52 | # Yalc 53 | .yalc 54 | yalc.lock 55 | 56 | # npm package zips 57 | *.tgz 58 | 59 | # Compiled plugin 60 | dist 61 | 62 | #vim 63 | *.swp 64 | *.swo 65 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npx --no -- commitlint --edit "" 5 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npx lint-staged 5 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .babelrc 2 | .editorconfig 3 | .eslintignore 4 | .eslintrc 5 | src 6 | test 7 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | dist 2 | pnpm-lock.yaml 3 | yarn.lock 4 | package-lock.json 5 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": false, 3 | "printWidth": 100, 4 | "bracketSpacing": false, 5 | "singleQuote": true 6 | } 7 | -------------------------------------------------------------------------------- /.releaserc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@sanity/semantic-release-preset", 3 | "branches": ["main"] 4 | } 5 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # 📓 Changelog 4 | 5 | All notable changes to this project will be documented in this file. See 6 | [Conventional Commits](https://conventionalcommits.org) for commit guidelines. 7 | 8 | ## [3.0.5](https://github.com/sanity-io/rich-date-input/compare/v3.0.4...v3.0.5) (2024-12-18) 9 | 10 | ### Bug Fixes 11 | 12 | - make react 19 compatible ([#19](https://github.com/sanity-io/rich-date-input/issues/19)) ([6a55154](https://github.com/sanity-io/rich-date-input/commit/6a551547b79977be1570d2b639f6fbd68f018222)) 13 | 14 | ## [3.0.4](https://github.com/sanity-io/rich-date-input/compare/v3.0.3...v3.0.4) (2024-07-18) 15 | 16 | ### Bug Fixes 17 | 18 | - recalculate offsets for possible DST effects ([#13](https://github.com/sanity-io/rich-date-input/issues/13)) ([5d97007](https://github.com/sanity-io/rich-date-input/commit/5d97007f0ab5efa129c7b9ca916e80bee10d91fe)) 19 | 20 | ## [3.0.3](https://github.com/sanity-io/rich-date-input/compare/v3.0.2...v3.0.3) (2024-06-26) 21 | 22 | ### Bug Fixes 23 | 24 | - **deps:** update deps ([df987ff](https://github.com/sanity-io/rich-date-input/commit/df987ffd8c8eba590782bae6151a229c7868a317)) 25 | 26 | ## [3.0.2](https://github.com/sanity-io/rich-date-input/compare/v3.0.1...v3.0.2) (2024-06-26) 27 | 28 | ### Bug Fixes 29 | 30 | - add missing peer dependency `date-fns` ([9aecf00](https://github.com/sanity-io/rich-date-input/commit/9aecf00400ff26d2e114e824a8248d4eea5fb9a8)) 31 | 32 | ## [3.0.1](https://github.com/sanity-io/rich-date-input/compare/v3.0.0...v3.0.1) (2024-04-14) 33 | 34 | ### Bug Fixes 35 | 36 | - refer to group names for timezone selection as well ([#7](https://github.com/sanity-io/rich-date-input/issues/7)) ([e543f11](https://github.com/sanity-io/rich-date-input/commit/e543f1138703d547b6a56aaf2f4a5e7ad4ffa326)) 37 | 38 | ## 1.0.0 (2023-11-21) 39 | 40 | ### ⚠ BREAKING CHANGES 41 | 42 | - upgrade to v3 (#5) 43 | 44 | ### Features 45 | 46 | - upgrade to v3 ([#5](https://github.com/sanity-io/rich-date-input/issues/5)) ([3893ded](https://github.com/sanity-io/rich-date-input/commit/3893dedf3de4d84312320202e318efcd0a2d4959)) 47 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Sanity.io 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | > This is a **Sanity Studio v3** plugin. 2 | > For the v2 version, please refer to the [studio-v2 branch](https://github.com/sanity-io/rich-date-input/tree/studio-v2). 3 | 4 | # V3 Rich Date Input 5 | 6 | Provides a timezone-aware date input for Sanity Studio. 7 | 8 | ![This is an image](assets/plugin.gif) 9 | 10 | ## Installation 11 | 12 | ```sh 13 | npm install @sanity/rich-date-input 14 | ``` 15 | 16 | ## Usage 17 | 18 | Add it as a plugin in `sanity.config.ts` (or .js): 19 | 20 | ```ts 21 | import {defineConfig} from 'sanity' 22 | import {richDate} from '@sanity/rich-date-input' 23 | 24 | export default defineConfig({ 25 | // ... 26 | plugins: [richDate()], 27 | }) 28 | ``` 29 | 30 | Then, use `richDate` as a type in your schema: 31 | 32 | ```ts 33 | import {defineField, defineType} from 'sanity' 34 | 35 | export default defineType({ 36 | name: 'event', 37 | title: 'Event', 38 | type: 'document', 39 | fields: [ 40 | defineField({ 41 | name: 'scheduledAt', 42 | title: 'Scheduled at', 43 | type: 'richDate', 44 | // this will take the same options available on the datetime type: https://www.sanity.io/docs/datetime-type 45 | options: { 46 | timeStep: 30, 47 | }, 48 | }), 49 | ], 50 | }) 51 | ``` 52 | 53 | When a user selects a date, the timezone will be stored in the document. They can choose a different timezone, if desired. The date displayed will be the time as it would be in that timezone. UTC will be calculated from the timezone and local time. 54 | 55 | The typical data output should be: 56 | 57 | ```ts 58 | { 59 | _type: 'richDate', 60 | local: '2023-02-21T10:15:00+01:00', 61 | utc: '2023-02-12T09:15:00Z', 62 | timezone: 'Europe/Oslo', 63 | offset: 60 64 | } 65 | ``` 66 | 67 | ## License 68 | 69 | [MIT](LICENSE) © Sanity.io 70 | 71 | ## Develop & test 72 | 73 | This plugin uses [@sanity/plugin-kit](https://github.com/sanity-io/plugin-kit) 74 | with default configuration for build & watch scripts. 75 | 76 | See [Testing a plugin in Sanity Studio](https://github.com/sanity-io/plugin-kit#testing-a-plugin-in-sanity-studio) 77 | on how to run this plugin with hotreload in the studio. 78 | 79 | ### Release new version 80 | 81 | Run ["CI & Release" workflow](https://github.com/sanity-io/v3-rich-date-input/actions/workflows/main.yml). 82 | Make sure to select the main branch and check "Release new version". 83 | 84 | Semantic release will only release on configured branches, so it is safe to run release on any branch. 85 | -------------------------------------------------------------------------------- /assets/plugin.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sanity-io/rich-date-input/86262c0b6e6a214d75603eb3c65dae96537ab2e6/assets/plugin.gif -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['@commitlint/config-conventional'], 3 | } 4 | -------------------------------------------------------------------------------- /lint-staged.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | '**/*.{js,jsx}': ['eslint'], 3 | '**/*.{ts,tsx}': ['eslint', () => 'tsc --build'], 4 | } 5 | -------------------------------------------------------------------------------- /package.config.ts: -------------------------------------------------------------------------------- 1 | import {defineConfig} from '@sanity/pkg-utils' 2 | 3 | export default defineConfig({ 4 | legacyExports: true, 5 | dist: 'dist', 6 | tsconfig: 'tsconfig.dist.json', 7 | 8 | // Remove this block to enable strict export validation 9 | extract: { 10 | rules: { 11 | 'ae-forgotten-export': 'off', 12 | 'ae-incompatible-release-tags': 'off', 13 | 'ae-internal-missing-underscore': 'off', 14 | 'ae-missing-release-tag': 'off', 15 | }, 16 | }, 17 | }) 18 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@sanity/rich-date-input", 3 | "version": "3.0.5", 4 | "description": "A timezone-aware datetime type and input component for Sanity Studio", 5 | "keywords": [ 6 | "sanity", 7 | "cms", 8 | "headless", 9 | "realtime", 10 | "content", 11 | "rich-date-input", 12 | "sanity-plugin" 13 | ], 14 | "homepage": "https://github.com/sanity-io/rich-date-input#readme", 15 | "bugs": { 16 | "url": "https://github.com/sanity-io/rich-date-input/issues" 17 | }, 18 | "repository": { 19 | "type": "git", 20 | "url": "git+ssh://git@github.com/sanity-io/rich-date-input.git" 21 | }, 22 | "license": "MIT", 23 | "author": "Sanity.io ", 24 | "exports": { 25 | ".": { 26 | "types": "./dist/index.d.ts", 27 | "source": "./src/index.ts", 28 | "require": "./dist/index.js", 29 | "import": "./dist/index.esm.js", 30 | "default": "./dist/index.esm.js" 31 | }, 32 | "./package.json": "./package.json" 33 | }, 34 | "main": "./dist/index.js", 35 | "module": "./dist/index.esm.js", 36 | "source": "./src/index.ts", 37 | "types": "./dist/index.d.ts", 38 | "files": [ 39 | "dist", 40 | "sanity.json", 41 | "src", 42 | "v2-incompatible.js" 43 | ], 44 | "scripts": { 45 | "build": "run-s clean && plugin-kit verify-package --silent && pkg-utils build --strict && pkg-utils --strict", 46 | "clean": "rimraf dist", 47 | "format": "prettier --write --cache --ignore-unknown .", 48 | "link-watch": "plugin-kit link-watch", 49 | "lint": "eslint .", 50 | "prepublishOnly": "run-s build", 51 | "watch": "pkg-utils watch --strict", 52 | "prebuild": "plugin-kit verify-package --silent && pkg-utils", 53 | "prepare": "husky install" 54 | }, 55 | "dependencies": { 56 | "@sanity/icons": "^3.5.3", 57 | "@sanity/incompatible-plugin": "^1.0.5", 58 | "@sanity/ui": "^2.10.11", 59 | "@vvo/tzdb": "^6.154.0", 60 | "date-fns": "^2.30.0", 61 | "date-fns-tz": "^2.0.1" 62 | }, 63 | "devDependencies": { 64 | "@commitlint/cli": "^18.4.1", 65 | "@commitlint/config-conventional": "^18.4.0", 66 | "@sanity/pkg-utils": "^3.3.8", 67 | "@sanity/plugin-kit": "^3.1.10", 68 | "@sanity/semantic-release-preset": "^4.1.6", 69 | "@types/react": "^18.3.3", 70 | "@typescript-eslint/eslint-plugin": "^6.21.0", 71 | "@typescript-eslint/parser": "^6.21.0", 72 | "eslint": "^8.57.0", 73 | "eslint-config-prettier": "^9.1.0", 74 | "eslint-config-sanity": "^7.1.2", 75 | "eslint-plugin-prettier": "^5.1.3", 76 | "eslint-plugin-react": "^7.34.3", 77 | "eslint-plugin-react-hooks": "^4.6.2", 78 | "husky": "^8.0.3", 79 | "lint-staged": "^15.2.7", 80 | "npm-run-all": "^4.1.5", 81 | "prettier": "^3.3.2", 82 | "prettier-plugin-packagejson": "^2.5.0", 83 | "react": "^18.3.1", 84 | "react-dom": "^18.3.1", 85 | "react-is": "^18.3.1", 86 | "rimraf": "^5.0.7", 87 | "sanity": "^3.48.0", 88 | "styled-components": "^6.1", 89 | "typescript": "^5.5.2" 90 | }, 91 | "peerDependencies": { 92 | "react": "^18 || ^19", 93 | "sanity": "^3" 94 | }, 95 | "engines": { 96 | "node": ">=14" 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "github>sanity-io/renovate-presets//ecosystem/auto", 5 | "github>sanity-io/renovate-presets//ecosystem/studio-v3" 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /sanity.json: -------------------------------------------------------------------------------- 1 | { 2 | "parts": [ 3 | { 4 | "implements": "part:@sanity/base/sanity-root", 5 | "path": "./v2-incompatible.js" 6 | } 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /src/components/RelativeDateTimePicker.tsx: -------------------------------------------------------------------------------- 1 | import {formatInTimeZone, getTimezoneOffset, zonedTimeToUtc} from 'date-fns-tz' 2 | import {type ReactNode, useCallback} from 'react' 3 | import {DateTimeInput, FormPatch, InputProps, PatchEvent, set, unset} from 'sanity' 4 | 5 | import {RichDate} from '../types' 6 | import {getConstructedUTCDate, unlocalizeDateTime} from '../utils' 7 | 8 | interface RelativeDateTimePickerProps extends Omit { 9 | dateValue?: RichDate 10 | } 11 | export const RelativeDateTimePicker = (props: RelativeDateTimePickerProps): ReactNode => { 12 | const {dateValue: value, onChange} = props 13 | 14 | const handleDateChange = useCallback( 15 | (patch: FormPatch | PatchEvent | FormPatch[]) => { 16 | const timezone = value?.timezone ?? Intl.DateTimeFormat().resolvedOptions().timeZone 17 | const newDatetime = (patch as unknown as {value: string})?.value 18 | if (!newDatetime || !('type' in patch) || patch.type !== 'set') { 19 | onChange(unset()) 20 | return 21 | } 22 | 23 | /* get what time the user "meant" to set without tz info 24 | * right now, newDatetime is the time the user set plus 25 | * their current offset, not the timezone offset 26 | */ 27 | const desiredDateTime = unlocalizeDateTime( 28 | newDatetime, 29 | Intl.DateTimeFormat().resolvedOptions().timeZone, 30 | ) 31 | 32 | const newUtcDateObject = zonedTimeToUtc(desiredDateTime, timezone) 33 | // offset may have changed based on DST, capture that 34 | const newOffset = getTimezoneOffset(timezone, newUtcDateObject) / 60 / 1000 35 | const localDate = formatInTimeZone(newUtcDateObject, timezone, "yyyy-MM-dd'T'HH:mm:ssXXX") 36 | 37 | const patches = [] 38 | 39 | patches.push(set(newUtcDateObject.toISOString(), ['utc'])) 40 | patches.push(set(localDate, ['local'])) 41 | 42 | if (!value?.timezone) { 43 | patches.push(set(timezone, ['timezone'])) 44 | } 45 | 46 | if (value?.offset !== newOffset) { 47 | patches.push(set(newOffset, ['offset'])) 48 | } 49 | 50 | onChange(patches) 51 | }, 52 | [onChange, value], 53 | ) 54 | 55 | const dateToDisplay = value?.utc ? getConstructedUTCDate(value.utc, value.offset) : '' 56 | 57 | // @ts-expect-error -- slight mismatch in elementProps and renderDefault, but should line up in practice 58 | return 59 | } 60 | -------------------------------------------------------------------------------- /src/components/RichDateInput.tsx: -------------------------------------------------------------------------------- 1 | import {Box, Dialog, Flex} from '@sanity/ui' 2 | import {type ReactNode, useCallback, useState} from 'react' 3 | import {ObjectInputMember, ObjectInputProps} from 'sanity' 4 | 5 | import {RichDate} from '../types' 6 | import {RelativeDateTimePicker} from './RelativeDateTimePicker' 7 | import {TimezoneButton} from './TimezoneButton' 8 | import {TimezoneSelector} from './TimezoneSelector' 9 | 10 | export const RichDateInput = (props: ObjectInputProps): ReactNode => { 11 | const {onChange, value, members, schemaType} = props 12 | const {options} = schemaType 13 | const localMember = members.find((member) => member.kind === 'field' && member.name === 'local') 14 | const timezoneMember = members.find( 15 | (member) => member.kind === 'field' && member.name === 'timezone', 16 | ) 17 | const [timezoneSelectorOpen, setTimezoneSelectorOpen] = useState(false) 18 | const onClose = useCallback(() => setTimezoneSelectorOpen(false), []) 19 | const onOpen = useCallback(() => setTimezoneSelectorOpen(true), []) 20 | 21 | return ( 22 | <> 23 | 24 | 25 | {localMember && ( 26 | ( 31 | 37 | )} 38 | /> 39 | )} 40 | 41 | 42 | {timezoneMember && ( 43 | ( 48 | 49 | )} 50 | /> 51 | )} 52 | 53 | 54 | {timezoneSelectorOpen && ( 55 | 56 | 57 | 58 | )} 59 | 60 | ) 61 | } 62 | -------------------------------------------------------------------------------- /src/components/TimezoneButton.tsx: -------------------------------------------------------------------------------- 1 | import {EarthAmericasIcon} from '@sanity/icons' 2 | import {Button} from '@sanity/ui' 3 | import {type ReactNode} from 'react' 4 | 5 | import {allTimezones} from '../utils' 6 | 7 | interface TimezoneButtonProps { 8 | onClick: () => void 9 | timezone: string 10 | } 11 | 12 | export const TimezoneButton = (props: TimezoneButtonProps): ReactNode => { 13 | const {onClick, timezone} = props 14 | const currentTimezone = Intl.DateTimeFormat().resolvedOptions().timeZone 15 | 16 | const label = 17 | allTimezones.find((tz) => tz.name === timezone)?.abbreviation ?? 18 | allTimezones.find((tz) => tz.name === currentTimezone)?.abbreviation ?? 19 | allTimezones.find((tz) => tz.group.includes(currentTimezone))?.abbreviation 20 | 21 | return ( 22 |