├── .husky └── pre-commit ├── .babelrc ├── .DS_Store ├── src ├── .DS_Store ├── index.ts ├── lib │ ├── i18n │ │ ├── cn.ts │ │ ├── en.ts │ │ ├── mg.ts │ │ ├── es.ts │ │ ├── fr.ts │ │ ├── pt.ts │ │ ├── de.ts │ │ └── vi.ts │ └── lib.ts ├── hooks │ ├── useOutput.ts │ ├── useDisplayTime.ts │ └── useDaysOfMonth.ts ├── components │ ├── Key.tsx │ ├── ModalFooter.tsx │ ├── ModalHandler.tsx │ ├── ModalHeader.tsx │ ├── NeatDatePicker.type.tsx │ ├── ChangeYearModal.tsx │ └── NeatDatePicker.tsx ├── dateformat.d.ts └── dateformat.js ├── .prettierrc ├── .npmignore ├── .gitignore ├── .eslintrc.json ├── tsconfig.json ├── LICENSE ├── package.json └── README.md /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | npx lint-staged -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["module:metro-react-native-babel-preset"] 3 | } -------------------------------------------------------------------------------- /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/roto93/react-native-neat-date-picker/HEAD/.DS_Store -------------------------------------------------------------------------------- /src/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/roto93/react-native-neat-date-picker/HEAD/src/.DS_Store -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "useTabs": false, 3 | "semi": false, 4 | "singleQuote": true, 5 | "jsxSingleQuote": true 6 | } 7 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | *.log 3 | npm-debug.log 4 | 5 | # Dependency directory 6 | node_modules 7 | 8 | # Runtime data 9 | tmp 10 | 11 | # Examples (If applicable to your project) 12 | examples -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | *.log 3 | npm-debug.log 4 | 5 | # typescript compiler output 6 | dist 7 | 8 | # vscode folder 9 | .vscode 10 | 11 | # Runtime data 12 | tmp 13 | build 14 | dist 15 | 16 | # Dependency directory 17 | node_modules 18 | 19 | *.tgz -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es2021": true 5 | }, 6 | "extends": ["plugin:react/jsx-runtime", "prettier"], 7 | "parser": "@typescript-eslint/parser", 8 | "plugins": ["react"], 9 | "parserOptions": { 10 | "ecmaFeatures": { 11 | "jsx": true 12 | }, 13 | "ecmaVersion": 2021, 14 | "sourceType": "module" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import DatePicker from './components/NeatDatePicker' 2 | import type { 3 | ColorOptions, 4 | NeatDatePickerProps, 5 | RangeOutput, 6 | SingleOutput, 7 | } from './components/NeatDatePicker.type' 8 | import type { i18nLanguageConfig, i18nLanguageKey } from './lib/lib' 9 | 10 | export default DatePicker 11 | export type { 12 | ColorOptions, 13 | i18nLanguageConfig, 14 | i18nLanguageKey, 15 | NeatDatePickerProps, 16 | RangeOutput, 17 | SingleOutput, 18 | } 19 | -------------------------------------------------------------------------------- /src/lib/i18n/cn.ts: -------------------------------------------------------------------------------- 1 | import { i18nLanguageConfig } from '../lib' 2 | 3 | const languageConfig: i18nLanguageConfig = { 4 | months: { 5 | '0': '一月', 6 | '1': '二月', 7 | '2': '三月', 8 | '3': '四月', 9 | '4': '五月', 10 | '5': '六月', 11 | '6': '七月', 12 | '7': '八月', 13 | '8': '九月', 14 | '9': '十月', 15 | '10': '十一月', 16 | '11': '十二月', 17 | }, 18 | weekDays: ['日', '一', '二', '三', '四', '五', '六'], 19 | accept: '確定', 20 | cancel: '取消', 21 | } 22 | 23 | export default languageConfig 24 | -------------------------------------------------------------------------------- /src/lib/i18n/en.ts: -------------------------------------------------------------------------------- 1 | import { i18nLanguageConfig } from '../lib' 2 | 3 | const languageConfig: i18nLanguageConfig = { 4 | months: { 5 | '0': 'Jan', 6 | '1': 'Feb', 7 | '2': 'Mar', 8 | '3': 'Apr', 9 | '4': 'May', 10 | '5': 'Jun', 11 | '6': 'Jul', 12 | '7': 'Aug', 13 | '8': 'Sep', 14 | '9': 'Oct', 15 | '10': 'Nov', 16 | '11': 'Dec', 17 | }, 18 | weekDays: ['S', 'M', 'T', 'W', 'T', 'F', 'S'], 19 | accept: 'OK', 20 | cancel: 'Cancel', 21 | } 22 | 23 | export default languageConfig 24 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "jsx": "react-jsx", 4 | "target": "es2016", 5 | "module": "commonjs", 6 | "resolveJsonModule": true, 7 | "declaration": true, 8 | "sourceMap": true, 9 | "outDir": "./dist", 10 | "esModuleInterop": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "strict": true, 13 | "noImplicitAny": false, 14 | "skipLibCheck": true 15 | }, 16 | "include": [ 17 | "src/**/*", 18 | ], 19 | "exclude": [ 20 | "node_modules" 21 | ] 22 | } -------------------------------------------------------------------------------- /src/lib/i18n/mg.ts: -------------------------------------------------------------------------------- 1 | import { i18nLanguageConfig } from '../lib' 2 | 3 | const languageConfig: i18nLanguageConfig = { 4 | months: { 5 | '0': 'Jan', 6 | '1': 'Feb', 7 | '2': 'Mar', 8 | '3': 'Avr', 9 | '4': 'May', 10 | '5': 'Jio', 11 | '6': 'Jol', 12 | '7': 'Aog', 13 | '8': 'Sep', 14 | '9': 'Okt', 15 | '10': 'Nov', 16 | '11': 'Des', 17 | }, 18 | weekDays: ['A', 'A', 'T', 'A', 'A', 'Z', 'S'], 19 | accept: 'Ekena', 20 | cancel: 'Averina', 21 | } 22 | 23 | export default languageConfig 24 | -------------------------------------------------------------------------------- /src/lib/i18n/es.ts: -------------------------------------------------------------------------------- 1 | import { i18nLanguageConfig } from '../lib' 2 | 3 | const languageConfig: i18nLanguageConfig = { 4 | months: { 5 | '0': 'Ene', 6 | '1': 'Feb', 7 | '2': 'Mar', 8 | '3': 'Abr', 9 | '4': 'May', 10 | '5': 'Jun', 11 | '6': 'Jul', 12 | '7': 'Ago', 13 | '8': 'Sep', 14 | '9': 'Oct', 15 | '10': 'Nov', 16 | '11': 'Dic', 17 | }, 18 | weekDays: ['D', 'L', 'M', 'X', 'J', 'V', 'S'], 19 | accept: 'Aceptar', 20 | cancel: 'Cancelar', 21 | } 22 | 23 | export default languageConfig 24 | -------------------------------------------------------------------------------- /src/lib/i18n/fr.ts: -------------------------------------------------------------------------------- 1 | import { i18nLanguageConfig } from '../lib' 2 | 3 | const languageConfig: i18nLanguageConfig = { 4 | months: { 5 | '0': 'Jan', 6 | '1': 'Fév', 7 | '2': 'Mar', 8 | '3': 'Avr', 9 | '4': 'Mai', 10 | '5': 'Jui', 11 | '6': 'Juil', 12 | '7': 'Aôu', 13 | '8': 'Sep', 14 | '9': 'Oct', 15 | '10': 'Nov', 16 | '11': 'Déc', 17 | }, 18 | weekDays: ['D', 'L', 'M', 'M', 'J', 'V', 'S'], 19 | accept: 'Accepter', 20 | cancel: 'Annuler', 21 | } 22 | 23 | export default languageConfig 24 | -------------------------------------------------------------------------------- /src/lib/i18n/pt.ts: -------------------------------------------------------------------------------- 1 | import { i18nLanguageConfig } from '../lib' 2 | 3 | const languageConfig: i18nLanguageConfig = { 4 | months: { 5 | '0': 'Jan', 6 | '1': 'Fev', 7 | '2': 'Mar', 8 | '3': 'Abr', 9 | '4': 'Mai', 10 | '5': 'Jun', 11 | '6': 'Jul', 12 | '7': 'Ago', 13 | '8': 'Set', 14 | '9': 'Out', 15 | '10': 'Nov', 16 | '11': 'Dez', 17 | }, 18 | weekDays: ['D', 'S', 'T', 'Q', 'Q', 'S', 'S'], 19 | accept: 'Aceitar', 20 | cancel: 'Cancelar', 21 | } 22 | 23 | export default languageConfig 24 | -------------------------------------------------------------------------------- /src/lib/i18n/de.ts: -------------------------------------------------------------------------------- 1 | import { i18nLanguageConfig } from '../lib' 2 | 3 | const languageConfig: i18nLanguageConfig = { 4 | months: { 5 | '0': 'Jan', 6 | '1': 'Feb', 7 | '2': 'Mär', 8 | '3': 'Apr', 9 | '4': 'Mai', 10 | '5': 'Jun', 11 | '6': 'Jul', 12 | '7': 'Aug', 13 | '8': 'Sep', 14 | '9': 'Okt', 15 | '10': 'Nov', 16 | '11': 'Dez', 17 | }, 18 | weekDays: ['So', 'Mo', 'Di', 'Mi', 'Do', 'Fr', 'Sa'], 19 | accept: 'Akzeptieren', 20 | cancel: 'Schließen', 21 | } 22 | 23 | export default languageConfig 24 | -------------------------------------------------------------------------------- /src/lib/i18n/vi.ts: -------------------------------------------------------------------------------- 1 | import { i18nLanguageConfig } from '../lib' 2 | 3 | const languageConfig: i18nLanguageConfig = { 4 | months: { 5 | '0': 'Thg 1', 6 | '1': 'Thg 2', 7 | '2': 'Thg 3', 8 | '3': 'Thg 4', 9 | '4': 'Thg 5', 10 | '5': 'Thg 6', 11 | '6': 'Thg 7', 12 | '7': 'Thg 8', 13 | '8': 'Thg 9', 14 | '9': 'Thg 10', 15 | '10': 'Thg 11', 16 | '11': 'Thg 12', 17 | }, 18 | weekDays: ['CN', 'T2', 'T3', 'T4', 'T5', 'T6', 'T7'], 19 | accept: 'Xác nhận', 20 | cancel: 'Hủy bỏ', 21 | } 22 | 23 | export default languageConfig 24 | -------------------------------------------------------------------------------- /src/hooks/useOutput.ts: -------------------------------------------------------------------------------- 1 | import { useState } from 'react' 2 | import { 3 | Mode, 4 | RangeOutput, 5 | SingleOutput, 6 | } from '../components/NeatDatePicker.type' 7 | 8 | const useOutput = ( 9 | mode: Mode, 10 | today: Date, 11 | startDate?: Date, 12 | endDate?: Date, 13 | ) => { 14 | // output decides which date should be active. 15 | const [output, setOutput] = useState( 16 | mode === 'single' 17 | ? { date: today, startDate: undefined, endDate: undefined } 18 | : { date: undefined, startDate, endDate }, 19 | ) 20 | 21 | // If user presses cancel, reset 'output' state to this 'originalOutput' 22 | const [originalOutput, setOriginalOutput] = useState(output) 23 | 24 | return { 25 | output, 26 | setOutput, 27 | originalOutput, 28 | setOriginalOutput, 29 | } 30 | } 31 | 32 | export default useOutput 33 | 34 | type Output = SingleOutput | RangeOutput 35 | -------------------------------------------------------------------------------- /src/lib/lib.ts: -------------------------------------------------------------------------------- 1 | import cn from './i18n/cn' 2 | import de from './i18n/de' 3 | import en from './i18n/en' 4 | import es from './i18n/es' 5 | import fr from './i18n/fr' 6 | import mg from './i18n/mg' 7 | import pt from './i18n/pt' 8 | import vi from './i18n/vi' 9 | 10 | export type i18nLanguageKey = 11 | | 'cn' 12 | | 'de' 13 | | 'en' 14 | | 'es' 15 | | 'fr' 16 | | 'mg' 17 | | 'pt' 18 | | 'vi' 19 | 20 | export type i18nLanguageConfig = { 21 | months: { 22 | '0': string 23 | '1': string 24 | '2': string 25 | '3': string 26 | '4': string 27 | '5': string 28 | '6': string 29 | '7': string 30 | '8': string 31 | '9': string 32 | '10': string 33 | '11': string 34 | } 35 | weekDays: [string, string, string, string, string, string, string] 36 | accept: string 37 | cancel: string 38 | } 39 | 40 | export const TranslationMap = { cn, de, en, es, fr, mg, pt, vi } 41 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Guan-Ting Su 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 | -------------------------------------------------------------------------------- /src/components/Key.tsx: -------------------------------------------------------------------------------- 1 | import { memo, type FC } from 'react' 2 | import { 3 | ColorValue, 4 | StyleSheet, 5 | Text, 6 | TouchableOpacity, 7 | View, 8 | } from 'react-native' 9 | import { Day } from './NeatDatePicker.type' 10 | 11 | type KeyProps = { 12 | Day: Day 13 | bgc: ColorValue 14 | textColor: ColorValue 15 | onKeyPress: (day: Day) => void 16 | } 17 | 18 | const Key: FC = ({ Day, bgc, textColor, onKeyPress }: KeyProps) => { 19 | return ( 20 | onKeyPress(Day)} style={styles.pressArea}> 21 | 22 | 23 | {' '} 24 | {Day.date}{' '} 25 | 26 | 27 | 28 | ) 29 | } 30 | 31 | const styles = StyleSheet.create({ 32 | pressArea: { 33 | paddingTop: 4, 34 | paddingHorizontal: 4, 35 | }, 36 | keys: { 37 | width: 34, 38 | height: 34, 39 | borderRadius: 10, 40 | justifyContent: 'center', 41 | alignItems: 'center', 42 | }, 43 | keys_text: { 44 | fontSize: 16, 45 | fontWeight: '500', 46 | // fontFamily: 'Roboto_500Medium' 47 | }, 48 | }) 49 | 50 | export default memo(Key) 51 | -------------------------------------------------------------------------------- /src/components/ModalFooter.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from 'react' 2 | import { StyleSheet, Text, TouchableOpacity, View } from 'react-native' 3 | import { i18nLanguageConfig } from '../lib/lib' 4 | import { ColorOptions } from './NeatDatePicker.type' 5 | 6 | interface Prop { 7 | colors: ColorOptions 8 | translation: i18nLanguageConfig 9 | onConfirmPress: () => void 10 | onCancelPress: () => void 11 | } 12 | 13 | const ModalFooter: FC = ({ 14 | translation, 15 | colors, 16 | onConfirmPress, 17 | onCancelPress, 18 | }) => { 19 | const { confirmButtonColor } = colors 20 | return ( 21 | 22 | 23 | 24 | {translation.cancel} 25 | 26 | 27 | 28 | {translation.accept} 29 | 30 | 31 | 32 | 33 | ) 34 | } 35 | 36 | export default ModalFooter 37 | 38 | const styles = StyleSheet.create({ 39 | footer: { 40 | width: 300, 41 | height: 52, 42 | flexDirection: 'row', 43 | justifyContent: 'flex-end', 44 | }, 45 | btn_box: { 46 | width: '100%', 47 | flexDirection: 'row', 48 | alignItems: 'center', 49 | justifyContent: 'space-between', 50 | }, 51 | btn: { 52 | minWidth: 60, 53 | height: '100%', 54 | paddingHorizontal: 8, 55 | justifyContent: 'center', 56 | alignItems: 'center', 57 | }, 58 | btn_text: { 59 | fontSize: 18, 60 | color: '#777', 61 | }, 62 | }) 63 | -------------------------------------------------------------------------------- /src/components/ModalHandler.tsx: -------------------------------------------------------------------------------- 1 | import { FC, ReactNode } from 'react' 2 | import { Dimensions, StyleSheet, ViewStyle } from 'react-native' 3 | import Modal from 'react-native-modal' 4 | 5 | /** 6 | * Change window height to screen height due to an issue in android. 7 | * 8 | * @issue https://github.com/react-native-modal/react-native-modal/issues/147#issuecomment-610729725 9 | */ 10 | const winY = Dimensions.get('screen').height 11 | 12 | interface Prop { 13 | children: ReactNode 14 | isVisible: boolean 15 | withoutModal?: boolean 16 | modalStyles: ViewStyle 17 | onBackButtonPress?: () => void 18 | onBackdropPress?: () => void 19 | onCancelPress?: () => void 20 | } 21 | 22 | const ModalHandler: FC = ({ 23 | children, 24 | isVisible, 25 | withoutModal, 26 | modalStyles, 27 | onBackButtonPress, 28 | onBackdropPress, 29 | onCancelPress, 30 | }) => { 31 | if (withoutModal) return children 32 | return ( 33 | 49 | {children} 50 | 51 | ) 52 | } 53 | 54 | export default ModalHandler 55 | 56 | const styles = StyleSheet.create({ 57 | modal: { 58 | flex: 1, 59 | alignItems: 'center', 60 | padding: 0, 61 | margin: 0, 62 | }, 63 | }) 64 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-native-neat-date-picker", 3 | "version": "1.6.1", 4 | "description": "An easy-to-use date picker for React Native", 5 | "main": "./src/index.ts", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "build": "tsc", 9 | "format": "prettier --write \"./src/**/*.{js,jsx,mjs,cjs,ts,tsx,json}\"", 10 | "prepare": "husky" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/roto93/react-native-neat-date-picker.git" 15 | }, 16 | "keywords": [ 17 | "react-native", 18 | "date", 19 | "datePicker", 20 | "date-picker", 21 | "date", 22 | "picker", 23 | "react", 24 | "native" 25 | ], 26 | "author": "roto93", 27 | "contributors": [ 28 | { 29 | "name": "diecodev", 30 | "email": "diegoddiaz05@gmail.com", 31 | "url": "https://diecodev.vercel.app" 32 | } 33 | ], 34 | "license": "MIT", 35 | "bugs": { 36 | "url": "https://github.com/roto93/react-native-neat-date-picker/issues" 37 | }, 38 | "homepage": "https://github.com/roto93/react-native-neat-date-picker#readme", 39 | "devDependencies": { 40 | "@types/react": "^19.0.7", 41 | "@types/react-native": "^0.73.0", 42 | "@types/react-native-vector-icons": "^6.4.18", 43 | "@typescript-eslint/eslint-plugin": "^8.21.0", 44 | "@typescript-eslint/parser": "^8.21.0", 45 | "eslint": "^8.57.1", 46 | "eslint-config-prettier": "^9.1.0", 47 | "eslint-config-standard": "^17.1.0", 48 | "eslint-plugin-import": "^2.31.0", 49 | "eslint-plugin-n": "^15.7.0", 50 | "eslint-plugin-promise": "^6.5.1", 51 | "eslint-plugin-react": "^7.37.4", 52 | "husky": "^9.1.7", 53 | "lint-staged": "^15.5.1", 54 | "metro-react-native-babel-preset": "^0.77.0", 55 | "prettier": "3.5.3", 56 | "typescript": "^5.8.3" 57 | }, 58 | "dependencies": { 59 | "react-native-modal": "14.0.0-rc.1", 60 | "react-native-vector-icons": "^10.2.0" 61 | }, 62 | "lint-staged": { 63 | "*.{js,jsx,ts,tsx}": [ 64 | "eslint --fix", 65 | "prettier --write" 66 | ] 67 | } 68 | } -------------------------------------------------------------------------------- /src/hooks/useDisplayTime.ts: -------------------------------------------------------------------------------- 1 | import { useCallback, useState } from 'react' 2 | 3 | /** 4 | * displayTime defines which month is going to be shown onto the screen 5 | * 6 | * For 'single' mode, displayTime is also the initial selected date when opening DatePicker at the first time. 7 | * @param initialDate Date 8 | */ 9 | const useDisplayTime = (initialDate?: Date, minDate?: Date, maxDate?: Date) => { 10 | const [displayTime, setDisplayTime] = useState(initialDate ?? new Date()) 11 | 12 | const displayYear = displayTime.getFullYear() 13 | const displayMonth = displayTime.getMonth() // 0-base 14 | const displayDate = displayTime.getDate() 15 | 16 | const TODAY = new Date(displayYear, displayMonth, displayDate) 17 | 18 | const currentMonthLastDate = new Date(displayYear, displayMonth + 1, 0) 19 | const firstDisplayDate = new Date(displayYear, displayMonth, 1) 20 | 21 | const canGoPreviousMonth = 22 | firstDisplayDate === undefined || 23 | minDate === undefined || 24 | minDate < firstDisplayDate 25 | const canGoNextMonth = 26 | maxDate === undefined || 27 | currentMonthLastDate === undefined || 28 | maxDate > currentMonthLastDate 29 | 30 | const goToDate = (year: number, month: number, date: number) => { 31 | const newDate = new Date(year, month, date) 32 | if (minDate && newDate < minDate) return setDisplayTime(minDate) 33 | if (maxDate && newDate > maxDate) return setDisplayTime(maxDate) 34 | setDisplayTime(new Date(year, month, date)) 35 | } 36 | 37 | const toPrevMonth = useCallback(() => { 38 | if (!canGoPreviousMonth) return 39 | const { year, month, date } = destructureDisplayTime(displayTime) 40 | goToDate(year, month - 1, date) 41 | }, [displayTime]) 42 | const toNextMonth = useCallback(() => { 43 | if (!canGoNextMonth) return 44 | const { year, month, date } = destructureDisplayTime(displayTime) 45 | goToDate(year, month + 1, date) 46 | }, [displayTime]) 47 | 48 | return { 49 | displayTime, 50 | setDisplayTime, 51 | displayYear, 52 | displayMonth, 53 | displayDate, 54 | TODAY, 55 | toPrevMonth, 56 | toNextMonth, 57 | goToDate, 58 | canGoPreviousMonth, 59 | canGoNextMonth, 60 | } 61 | } 62 | 63 | export default useDisplayTime 64 | 65 | const destructureDisplayTime = (displayTime: Date) => { 66 | return { 67 | year: displayTime.getFullYear(), 68 | month: displayTime.getMonth(), // 0-base 69 | date: displayTime.getDate(), 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/hooks/useDaysOfMonth.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react' 2 | import { Day } from '../components/NeatDatePicker.type' 3 | 4 | /** 5 | * input date 6 | * 7 | * inputYear: 8 | * inputMonth: 0-base 9 | * dateArray: An array that contains same amount of number as how many days in inputMonth, inputYear. 10 | * Also contain last few days of the previous month and first few days of the next month. 11 | * eg. 2021Feb starts from Monday and ends on Saturday, dateArray = [1,2,3,4,...,27,28] 12 | * 13 | * 14 | */ 15 | const useDaysOfMonth = ( 16 | inputYear: number, 17 | inputMonth: number, 18 | minTime?: number, 19 | maxTime?: number, 20 | ): Day[] => { 21 | const [dateArray, setDateArray] = useState([]) 22 | 23 | const days = new Date(inputYear, inputMonth + 1, 0).getDate() 24 | 25 | const firstDay = new Date(inputYear, inputMonth, 1).getDay() 26 | 27 | const prevMonthDays = new Date(inputYear, inputMonth, 0).getDate() 28 | 29 | const createDateArray = () => { 30 | let arr = Array.from(Array(days), (_, i) => { 31 | return { 32 | year: inputYear, 33 | month: inputMonth, 34 | date: i + 1, 35 | isCurrentMonth: true, 36 | disabled: false, 37 | } 38 | }) 39 | 40 | // 補上個月的日期 41 | let daysShouldInsert = firstDay 42 | let insertedNumber = prevMonthDays 43 | while (daysShouldInsert > 0 && daysShouldInsert < 7) { 44 | const insertingTime = { 45 | year: inputYear, 46 | month: inputMonth - 1, 47 | date: insertedNumber, 48 | isCurrentMonth: false, 49 | disabled: false, 50 | } 51 | arr.unshift(insertingTime) 52 | insertedNumber-- 53 | daysShouldInsert-- 54 | } 55 | 56 | // 補下個月的日期 57 | let blankInEnd = arr.length % 7 // 最後一行剩幾個空格 58 | if (blankInEnd !== 0) blankInEnd = blankInEnd - 7 // 如有餘數則再減七,得到要補的日期數量 59 | let i = -1 60 | while (i >= blankInEnd) { 61 | const insertingTime = { 62 | year: inputYear, 63 | month: inputMonth + 1, 64 | date: i * -1, 65 | isCurrentMonth: false, 66 | disabled: false, 67 | } 68 | 69 | arr.push({ ...insertingTime }) 70 | i-- 71 | } 72 | 73 | // 若有給上下限,把在範圍外的按鍵 disable 74 | if (minTime || maxTime) { 75 | const checkShouldDisabled = (day: Day) => { 76 | const thisKeyTime = new Date(day.year, day.month, day.date).getTime() 77 | let shouldDisableKey = false 78 | if (maxTime && thisKeyTime > maxTime) shouldDisableKey = true 79 | if (minTime && thisKeyTime < minTime) shouldDisableKey = true 80 | const disableKey = !!shouldDisableKey 81 | return { ...day, disabled: disableKey } 82 | } 83 | arr = arr.map(checkShouldDisabled) 84 | } 85 | 86 | return arr 87 | } 88 | 89 | useEffect(() => { 90 | setDateArray(createDateArray()) 91 | }, [inputYear, inputMonth, minTime, maxTime]) 92 | 93 | return dateArray 94 | } 95 | 96 | export default useDaysOfMonth 97 | -------------------------------------------------------------------------------- /src/components/ModalHeader.tsx: -------------------------------------------------------------------------------- 1 | import { Dispatch, FC, SetStateAction } from 'react' 2 | import { StyleSheet, Text, TouchableOpacity, View } from 'react-native' 3 | import MDicon from 'react-native-vector-icons/MaterialIcons' 4 | import { i18nLanguageConfig } from '../lib/lib' 5 | import { ColorOptions, Day } from './NeatDatePicker.type' 6 | 7 | interface Prop { 8 | days: Day[] 9 | colors: ColorOptions 10 | translation: i18nLanguageConfig 11 | canGoPreviousMonth: boolean 12 | canGoNextMonth: boolean 13 | toPrevMonth: () => void 14 | toNextMonth: () => void 15 | openYearModal: Dispatch> 16 | previousMonthIcon?: React.ReactNode 17 | nextMonthIcon?: React.ReactNode 18 | } 19 | 20 | const ModalHeader: FC = ({ 21 | days, 22 | colors, 23 | translation, 24 | canGoPreviousMonth, 25 | canGoNextMonth, 26 | toPrevMonth, 27 | toNextMonth, 28 | openYearModal, 29 | previousMonthIcon, 30 | nextMonthIcon, 31 | }) => { 32 | const { headerColor, headerTextColor } = colors 33 | return ( 34 | 35 | {/* last month */} 36 | 44 | {previousMonthIcon ? (previousMonthIcon) : } 49 | 50 | 51 | {/* displayed year and month */} 52 | openYearModal(true)}> 53 | 54 | {days.length !== 0 && days[10].year + ' '} 55 | { 56 | days.length !== 0 && 57 | translation?.months[days[10].month as unknown as '0'] // badly supress type error 58 | } 59 | 60 | 61 | 62 | {/* next month */} 63 | 68 | {nextMonthIcon ? (nextMonthIcon) : } 73 | 74 | 75 | ) 76 | } 77 | 78 | export default ModalHeader 79 | 80 | const styles = StyleSheet.create({ 81 | header: { 82 | flexDirection: 'row', 83 | width: '100%', 84 | height: 68, 85 | paddingHorizontal: 24, 86 | justifyContent: 'space-between', 87 | alignItems: 'center', 88 | marginBottom: 8, 89 | }, 90 | header__title: { 91 | fontSize: 24, 92 | color: '#fff', 93 | fontWeight: '500', 94 | }, 95 | changeMonthTO: { 96 | justifyContent: 'center', 97 | alignItems: 'center', 98 | width: 50, 99 | height: 50, 100 | padding: 4, 101 | borderColor: 'black', 102 | }, 103 | }) 104 | -------------------------------------------------------------------------------- /src/dateformat.d.ts: -------------------------------------------------------------------------------- 1 | // Type definitions for dateformat 5.0 2 | // Project: https://github.com/felixge/node-dateformat 3 | // Definitions by: Kombu 4 | // BendingBender 5 | // Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped 6 | 7 | export as namespace dateFormat 8 | 9 | /** 10 | * @param date Defaults to the current date/time. 11 | * @param mask Defaults to `masks.default`. 12 | * @returns A formatted version of the given date. 13 | */ 14 | export default function dateFormat( 15 | date?: Date | string | number, 16 | mask?: string, 17 | utc?: boolean, 18 | gmt?: boolean, 19 | ): string 20 | // eslint-disable-next-line no-redeclare 21 | export default function dateFormat( 22 | mask?: string, 23 | utc?: boolean, 24 | gmt?: boolean, 25 | ): string 26 | 27 | /** 28 | * Get proper timezone abbreviation or timezone offset. 29 | * 30 | * This will fall back to `GMT+xxxx` if it does not recognize the 31 | * timezone within the `timezone` RegEx above. Currently only common 32 | * American and Australian timezone abbreviations are supported. 33 | */ 34 | export function formatTimezone(date: string | Date): string 35 | 36 | export interface DateFormatMasks { 37 | /** 38 | * @default "ddd mmm dd yyyy HH:MM:ss" 39 | */ 40 | default: string 41 | /** 42 | * @default "m/d/yy" 43 | */ 44 | shortDate: string 45 | /** 46 | * @default "mm/dd/yyyy" 47 | */ 48 | paddedShortDate: string 49 | /** 50 | * @default "mmm d, yyyy" 51 | */ 52 | mediumDate: string 53 | /** 54 | * @default "mmmm d, yyyy" 55 | */ 56 | longDate: string 57 | /** 58 | * @default "dddd, mmmm d, yyyy" 59 | */ 60 | fullDate: string 61 | /** 62 | * @default "h:MM TT" 63 | */ 64 | shortTime: string 65 | /** 66 | * @default "h:MM:ss TT" 67 | */ 68 | mediumTime: string 69 | /** 70 | * @default "h:MM:ss TT Z" 71 | */ 72 | longTime: string 73 | /** 74 | * @default "yyyy-mm-dd" 75 | */ 76 | isoDate: string 77 | /** 78 | * @default "HH:MM:ss" 79 | */ 80 | isoTime: string 81 | /** 82 | * @default "yyyy-mm-dd'T'HH:MM:sso" 83 | */ 84 | isoDateTime: string 85 | /** 86 | * @default "UTC:yyyy-mm-dd'T'HH:MM:ss'Z'" 87 | */ 88 | isoUtcDateTime: string 89 | /** 90 | * @default "ddd, dd mmm yyyy HH:MM:ss Z" 91 | */ 92 | expiresHeaderFormat: string 93 | [key: string]: string 94 | } 95 | 96 | export interface DateFormatI18n { 97 | dayNames: string[] 98 | monthNames: string[] 99 | timeNames: string[] 100 | } 101 | 102 | /** 103 | * Predefined Formats 104 | */ 105 | export let masks: DateFormatMasks 106 | 107 | /** 108 | * Internationalization strings 109 | * 110 | * @example 111 | * import { i18n } from 'dateformat'; 112 | * 113 | * i18n.dayNames = [ 114 | * 'Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 115 | * 'Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday' 116 | * ]; 117 | * i18n.monthNames = [ 118 | * 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec', 119 | * 'January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December' 120 | * ]; 121 | * i18n.timeNames = [ 122 | * 'a', 'p', 'am', 'pm', 'A', 'P', 'AM', 'PM' 123 | * ]; 124 | */ 125 | export let i18n: DateFormatI18n 126 | -------------------------------------------------------------------------------- /src/components/NeatDatePicker.type.tsx: -------------------------------------------------------------------------------- 1 | import { ColorValue, ViewStyle } from 'react-native' 2 | import { i18nLanguageConfig, i18nLanguageKey } from '../lib/lib' 3 | 4 | export type Mode = 'single' | 'range' 5 | 6 | export type Day = { 7 | year: number 8 | month: number 9 | date: number 10 | isCurrentMonth: boolean 11 | disabled: boolean 12 | } 13 | 14 | export type ColorOptions = { 15 | /** The background color of date picker and that of change year modal. */ 16 | backgroundColor?: ColorValue 17 | 18 | /** The background color of header. */ 19 | headerColor?: ColorValue 20 | 21 | /** The color of texts and icons in header. */ 22 | headerTextColor?: ColorValue 23 | 24 | /** The text color of week days (like Monday, Tuesday ...) which shown below header. */ 25 | weekDaysColor?: ColorValue 26 | 27 | /** The text color of all the displayed date when not being selected. 28 | * 29 | * @abstract Only six-digits HEX code colors (like #ffffff. #fff won't work) are allowed because I do something like this behind the scene. 30 | */ 31 | dateTextColor?: ColorValue 32 | 33 | /** The text color of all the displayed date when being selected. 34 | * 35 | * @abstract Only six-digits HEX code colors (like #ffffff. #fff won't work) are allowed because I do something like this behind the scene. 36 | */ 37 | selectedDateTextColor?: ColorValue 38 | 39 | /** The background color of all the displayed date when being selected. 40 | * 41 | * @abstract Only six-digits HEX code colors (like #ffffff. #fff won't work) are allowed because I do something like this behind the scene. 42 | */ 43 | selectedDateBackgroundColor?: ColorValue 44 | 45 | /** The text color of the confirm Button. */ 46 | confirmButtonColor?: ColorValue 47 | 48 | /** The color of texts and icons in change year modal. */ 49 | changeYearModalColor?: ColorValue 50 | } 51 | 52 | export type NeatDatePickerCommonProps = { 53 | /** 54 | * Show or hide the date picker modal. 55 | * 56 | * @required 57 | */ 58 | isVisible: boolean 59 | 60 | /** 61 | * Sets the initial date displayed on the first open. 62 | */ 63 | initialDate?: Date 64 | 65 | /** 66 | * Specifies the latest selectable date. 67 | */ 68 | maxDate?: Date 69 | 70 | /** 71 | * Specifies the earliest selectable date. 72 | */ 73 | minDate?: Date 74 | 75 | /** 76 | * Custom styles for the modal. 77 | * 78 | * @type Object 79 | */ 80 | modalStyles?: ViewStyle 81 | 82 | /** 83 | * Built in language sets. 84 | */ 85 | language?: i18nLanguageKey 86 | 87 | /** 88 | * Custom language config. 89 | */ 90 | customLanguageConfig?: i18nLanguageConfig 91 | 92 | /** 93 | * The colorOptions prop contains several color settings. It helps you customize the date picker. 94 | * 95 | * @default { backgroundColor: '#ffffff', 96 | * headerColor: '#4682E9', 97 | * headerTextColor: '#ffffff', 98 | * changeYearModalColor: '#4682E9', 99 | * weekDaysColor: '#4682E9', 100 | * dateTextColor: '#000000', 101 | * selectedDateTextColor: '#ffffff', 102 | * selectedDateBackgroundColor: '#4682E9', 103 | * confirmButtonColor: '#4682E9' 104 | * } 105 | */ 106 | colorOptions?: ColorOptions 107 | 108 | /** 109 | * If true, the date picker will be displayed directly instead of being placed in a modal. 110 | */ 111 | withoutModal?: boolean 112 | 113 | /** 114 | * Set this prop to `true` if you want to pop up the year modal first. This will force the user to select the year before selecting the date. 115 | */ 116 | chooseYearFirst?: boolean 117 | 118 | /** 119 | * Specify the format of dateString. e.g.'yyyyMMdd', 'dd-MM-yyyy' 120 | * 121 | * @borrows This property use dateFormat library. you can find more information here: https://github.com/felixge/node-dateformat#mask-options but you can only use the mask part. 122 | */ 123 | dateStringFormat?: string 124 | 125 | 126 | /** 127 | * The icon to use for the previous month button. 128 | */ 129 | previousMonthIcon?: React.ReactNode 130 | 131 | /** 132 | * The icon to use for the next month button. 133 | */ 134 | nextMonthIcon?: React.ReactNode 135 | 136 | /** 137 | * This callback will execute when user presses cancel button. 138 | * 139 | * @required 140 | */ 141 | onCancel: () => void 142 | 143 | /** 144 | * A callback function which will be called when the backdrop is pressed. 145 | */ 146 | onBackdropPress?: () => void 147 | 148 | /** 149 | * A callback function which will be called when the Android back button is pressed. 150 | */ 151 | onBackButtonPress?: () => void 152 | } 153 | 154 | export type NeatSingleDatePickerProps = NeatDatePickerCommonProps & { 155 | /** 156 | * Set the type of date picker selection. 157 | * 158 | * @enum 'single' | 'range' 159 | * @required 160 | */ 161 | mode: 'single' 162 | 163 | /** 164 | * This callback will execute when user presses confirm button. 165 | * 166 | * this prop passes an argument `output` For 'single' mode, output contains two properties `date`, `dateString`. 167 | * As for 'range' mode, it contains four properties `startDate`, `startDateString`, `endDate` and `endDateString` 168 | * 169 | * @example 170 | * #### single mode: 171 | * 172 | * ```ts 173 | * const onConfirm = ({ date: Date, dateString: string }) => { 174 | * console.log(date.getTime()) 175 | * console.log(dateString) 176 | * } 177 | * ``` 178 | * 179 | * @required 180 | */ 181 | onConfirm: (output: SingleOutput) => void 182 | } 183 | export type NeatRangeDatePickerProps = NeatDatePickerCommonProps & { 184 | /** 185 | * Set the type of date picker selection. 186 | * 187 | * @enum 'single' | 'range' 188 | * @required 189 | */ 190 | mode: 'range' 191 | 192 | /** 193 | * Set this prop to a date if you need to set an initial starting date when opening the date picker the first time. Only works with 'range' mode. 194 | */ 195 | startDate?: Date 196 | 197 | /** 198 | * Set this prop to a date if you need to set a limit date when opening the date picker the first time. Only works with 'range' mode. 199 | */ 200 | endDate?: Date 201 | 202 | /** 203 | * This callback will execute when user presses confirm button. 204 | * 205 | * this prop passes an argument `output` For 'single' mode, output contains two properties `date`, `dateString`. 206 | * As for 'range' mode, it contains four properties `startDate`, `startDateString`, `endDate` and `endDateString` 207 | * 208 | * #### range mode: 209 | * 210 | * ```ts 211 | * const onConfirm = (output) => { 212 | * const {startDate, startDateString, endDate, endDateString} = output 213 | * console.log(startDate.getTime()) 214 | * console.log(startDateString) 215 | * console.log(endDate.getTime()) 216 | * console.log(endDateString) 217 | * } 218 | * ``` 219 | * 220 | * @required 221 | */ 222 | onConfirm: (output: RangeOutput) => void 223 | } 224 | 225 | export type SingleOutput = { 226 | date?: Date 227 | dateString?: string 228 | } 229 | 230 | export type RangeOutput = { 231 | startDate?: Date 232 | startDateString?: string 233 | endDate?: Date 234 | endDateString?: string 235 | } 236 | 237 | export type NeatDatePickerProps = 238 | | NeatSingleDatePickerProps 239 | | NeatRangeDatePickerProps 240 | -------------------------------------------------------------------------------- /src/components/ChangeYearModal.tsx: -------------------------------------------------------------------------------- 1 | import { FC, useRef, useEffect, useState, JSX } from 'react' 2 | import { 3 | ColorValue, 4 | FlatList, 5 | StyleSheet, 6 | Text, 7 | TouchableOpacity, 8 | View, 9 | ViewToken, 10 | } from 'react-native' 11 | import Modal, { ModalProps } from 'react-native-modal' 12 | import MDicon from 'react-native-vector-icons/MaterialIcons' 13 | 14 | /** 15 | * The number of years before and after the current year to generate in the year list. 16 | * For example, if the current year is 2025 and `YEARS_OFFSET` is 25, 17 | * the list will start from (2025 - 25) = 2000 and go up to (2000 + 49) = 2049. 18 | */ 19 | const YEARS_OFFSET = 25 20 | 21 | /** 22 | * The fixed height (in pixels) of each item (year) in the FlatList. 23 | * This value is used to calculate the scroll position and for snapping behavior. 24 | */ 25 | const ITEM_HEIGHT = 60 26 | 27 | export type ChangeYearModalProps = { 28 | /** 29 | * The color of texts, icons and the background color of change year modal. 30 | * @param primary: The color of texts and icons in change year modal. 31 | * @param backgroundColor:The background color of change year modal. 32 | */ 33 | colorOptions: { 34 | primary: ColorValue 35 | backgroundColor: ColorValue 36 | } 37 | /** 38 | * the functiont o excute when the modal is closed. 39 | */ 40 | dismiss: () => void 41 | // The current date to show in the modal. 42 | displayTime: Date 43 | // Prop to know if the modal is visible or not. 44 | isVisible: boolean 45 | 46 | /** 47 | * This is a extension from `ModalProps` from `react-native-modal` library. 48 | * If you want to know more about this prop, please visit: 49 | * 50 | * {@link https://github.com/react-native-modal/react-native-modal/blob/master/src/modal.tsx} 51 | */ 52 | changeYearModalProps?: Omit 53 | /** 54 | * Switch the date picker to the selected year. 55 | */ 56 | goToDate: (year: number, month: number, date: number) => void 57 | } 58 | 59 | /** 60 | * This is a component to change the year. 61 | * @param {ChangeYearModalProps.colorOptions} colorOptions - Is an object that receives two keys: `primary` and `backgroundColor`, their values change the color of the texts, icons and background of the modal. 62 | * @param {ChangeYearModalProps.dismiss} dismiss - Is a function that is executed when the modal is closed. 63 | * @param {ChangeYearModalProps.displayTime} displayTime - Is the current date to show in the modal. 64 | * @param {ChangeYearModalProps.isVisible} isVisible - Is a prop to know if the modal is visible or not. 65 | * @param goToDate Switch the date picker to the selected year, month, and date. 66 | * @param {ChangeYearModalProps.changeYearModalProps} changeYearModalProps - Is a prop that extends the `ModalProps` from `react-native-modal` library. 67 | * @returns {JSX.Element} Returns a JSX.Element. 68 | */ 69 | const ChangeYearModal: FC = ({ 70 | colorOptions, 71 | dismiss, 72 | displayTime, 73 | isVisible, 74 | goToDate, 75 | changeYearModalProps, 76 | }: ChangeYearModalProps): JSX.Element => { 77 | const { primary, backgroundColor } = colorOptions 78 | const currentYear = displayTime.getFullYear() 79 | const years = Array.from( 80 | { length: 50 }, 81 | (_, i) => currentYear - YEARS_OFFSET + i, 82 | ) 83 | 84 | const [selectedYear, setSelectedYear] = useState(currentYear) 85 | const flatListRef = useRef(null) 86 | 87 | const scrollToIndex = (index: number, animated = true) => { 88 | flatListRef.current?.scrollToIndex({ 89 | index, 90 | animated, 91 | viewPosition: 0.5, 92 | }) 93 | } 94 | 95 | const onDismiss = () => { 96 | dismiss() 97 | goToDate(selectedYear, displayTime.getMonth(), displayTime.getDate()) 98 | } 99 | 100 | useEffect(() => { 101 | if (isVisible) { 102 | setSelectedYear(currentYear) 103 | const index = years.findIndex((y) => y === currentYear) 104 | setTimeout(() => scrollToIndex(index, false), 0) 105 | } 106 | }, [isVisible]) 107 | 108 | const onViewableItemsChanged = useRef( 109 | ({ viewableItems }: { viewableItems: ViewToken[] }) => { 110 | if (viewableItems.length > 0) { 111 | const middleItem = viewableItems[Math.floor(viewableItems.length / 2)] 112 | if (middleItem?.item) setSelectedYear(middleItem.item) 113 | } 114 | }, 115 | ).current 116 | 117 | const getItemLayout = (_: any, index: number) => ({ 118 | length: ITEM_HEIGHT, 119 | offset: ITEM_HEIGHT * index, 120 | index, 121 | }) 122 | 123 | const changeYearBy = (delta: number) => { 124 | const newIndex = years.findIndex((y) => y === selectedYear) + delta 125 | if (newIndex >= 0 && newIndex < years.length) { 126 | scrollToIndex(newIndex) 127 | } 128 | } 129 | 130 | return ( 131 | 143 | 144 | changeYearBy(-1)} style={styles.btn}> 145 | 146 | 147 | 148 | item.toString()} 151 | ref={flatListRef} 152 | renderItem={({ item }) => ( 153 | 154 | 164 | {item} 165 | 166 | 167 | )} 168 | getItemLayout={getItemLayout} 169 | onViewableItemsChanged={onViewableItemsChanged} 170 | viewabilityConfig={{ itemVisiblePercentThreshold: 50 }} 171 | showsVerticalScrollIndicator={false} 172 | snapToInterval={ITEM_HEIGHT} 173 | decelerationRate='fast' 174 | bounces={false} 175 | /> 176 | 177 | changeYearBy(1)} style={styles.btn}> 178 | 179 | 180 | 181 | 182 | ) 183 | } 184 | 185 | export default ChangeYearModal 186 | 187 | const styles = StyleSheet.create({ 188 | modal: { 189 | justifyContent: 'center', 190 | alignItems: 'center', 191 | }, 192 | container: { 193 | height: ITEM_HEIGHT * 3 + 48 * 2, 194 | width: 250, 195 | borderRadius: 12, 196 | padding: 16, 197 | backgroundColor: '#fff', 198 | justifyContent: 'center', 199 | alignItems: 'center', 200 | }, 201 | btn: { 202 | width: 100, 203 | justifyContent: 'center', 204 | alignItems: 'center', 205 | }, 206 | btn_text: { 207 | fontSize: 18, 208 | }, 209 | yearText: { 210 | fontSize: 28, 211 | fontWeight: 'bold', 212 | textAlign: 'center', 213 | }, 214 | prevYearText: { 215 | fontSize: 16, 216 | color: '#7A7A7A', 217 | textAlign: 'center', 218 | marginTop: 8, 219 | marginBottom: 4, 220 | }, 221 | nextYearText: { 222 | fontSize: 16, 223 | color: '#7A7A7A', 224 | textAlign: 'center', 225 | marginTop: 4, 226 | marginBottom: 8, 227 | }, 228 | yearItem: { 229 | justifyContent: 'center', 230 | alignItems: 'center', 231 | }, 232 | }) 233 | -------------------------------------------------------------------------------- /src/dateformat.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Date Format 1.2.3 3 | * (c) 2007-2009 Steven Levithan 4 | * MIT license 5 | * 6 | * Includes enhancements by Scott Trenda 7 | * and Kris Kowal 8 | * 9 | * Accepts a date, a mask, or a date and a mask. 10 | * Returns a formatted version of the given date. 11 | * The date defaults to the current date/time. 12 | * The mask defaults to masks.default. 13 | */ 14 | 15 | // Regexes and supporting functions are cached through closure 16 | const token = 17 | /d{1,4}|D{3,4}|m{1,4}|yy(?:yy)?|([HhMsTt])\1?|W{1,2}|[LlopSZN]|"[^"]*"|'[^']*'/g 18 | const timezone = 19 | /\b(?:[A-Z]{1,3}[A-Z][TC])(?:[-+]\d{4})?|((?:Australian )?(?:Pacific|Mountain|Central|Eastern|Atlantic) (?:Standard|Daylight|Prevailing) Time)\b/g 20 | const timezoneClip = /[^-+\dA-Z]/g 21 | 22 | /** 23 | * @param {string | number | Date} date 24 | * @param {string} mask 25 | * @param {boolean} utc 26 | * @param {boolean} gmt 27 | */ 28 | export default function dateFormat(date, mask, utc, gmt) { 29 | // You can't provide utc if you skip other args (use the 'UTC:' mask prefix) 30 | if (arguments.length === 1 && typeof date === 'string' && !/\d/.test(date)) { 31 | mask = date 32 | date = undefined 33 | } 34 | 35 | date = date || date === 0 ? date : new Date() 36 | 37 | if (!(date instanceof Date)) { 38 | date = new Date(date) 39 | } 40 | 41 | if (isNaN(date)) { 42 | throw TypeError('Invalid date') 43 | } 44 | 45 | mask = String(masks[mask] || mask || masks.default) 46 | 47 | // Allow setting the utc/gmt argument via the mask 48 | const maskSlice = mask.slice(0, 4) 49 | if (maskSlice === 'UTC:' || maskSlice === 'GMT:') { 50 | mask = mask.slice(4) 51 | utc = true 52 | if (maskSlice === 'GMT:') { 53 | gmt = true 54 | } 55 | } 56 | 57 | const _ = () => (utc ? 'getUTC' : 'get') 58 | const d = () => date[_() + 'Date']() 59 | const D = () => date[_() + 'Day']() 60 | const m = () => date[_() + 'Month']() 61 | const y = () => date[_() + 'FullYear']() 62 | const H = () => date[_() + 'Hours']() 63 | const M = () => date[_() + 'Minutes']() 64 | const s = () => date[_() + 'Seconds']() 65 | const L = () => date[_() + 'Milliseconds']() 66 | const o = () => (utc ? 0 : date.getTimezoneOffset()) 67 | const W = () => getWeek(date) 68 | const N = () => getDayOfWeek(date) 69 | 70 | const flags = { 71 | d: () => d(), 72 | dd: () => pad(d()), 73 | ddd: () => i18n.dayNames[D()], 74 | DDD: () => 75 | getDayName({ 76 | y: y(), 77 | m: m(), 78 | d: d(), 79 | _: _(), 80 | dayName: i18n.dayNames[D()], 81 | short: true, 82 | }), 83 | dddd: () => i18n.dayNames[D() + 7], 84 | DDDD: () => 85 | getDayName({ 86 | y: y(), 87 | m: m(), 88 | d: d(), 89 | _: _(), 90 | dayName: i18n.dayNames[D() + 7], 91 | }), 92 | m: () => m() + 1, 93 | mm: () => pad(m() + 1), 94 | mmm: () => i18n.monthNames[m()], 95 | mmmm: () => i18n.monthNames[m() + 12], 96 | yy: () => String(y()).slice(2), 97 | yyyy: () => pad(y(), 4), 98 | h: () => H() % 12 || 12, 99 | hh: () => pad(H() % 12 || 12), 100 | H: () => H(), 101 | HH: () => pad(H()), 102 | M: () => M(), 103 | MM: () => pad(M()), 104 | s: () => s(), 105 | ss: () => pad(s()), 106 | l: () => pad(L(), 3), 107 | L: () => pad(Math.floor(L() / 10)), 108 | t: () => (H() < 12 ? i18n.timeNames[0] : i18n.timeNames[1]), 109 | tt: () => (H() < 12 ? i18n.timeNames[2] : i18n.timeNames[3]), 110 | T: () => (H() < 12 ? i18n.timeNames[4] : i18n.timeNames[5]), 111 | TT: () => (H() < 12 ? i18n.timeNames[6] : i18n.timeNames[7]), 112 | Z: () => (gmt ? 'GMT' : utc ? 'UTC' : formatTimezone(date)), 113 | o: () => 114 | (o() > 0 ? '-' : '+') + 115 | pad(Math.floor(Math.abs(o()) / 60) * 100 + (Math.abs(o()) % 60), 4), 116 | p: () => 117 | (o() > 0 ? '-' : '+') + 118 | pad(Math.floor(Math.abs(o()) / 60), 2) + 119 | ':' + 120 | pad(Math.floor(Math.abs(o()) % 60), 2), 121 | S: () => 122 | ['th', 'st', 'nd', 'rd'][ 123 | d() % 10 > 3 ? 0 : (((d() % 100) - (d() % 10) !== 10) * d()) % 10 124 | ], 125 | W: () => W(), 126 | WW: () => pad(W()), 127 | N: () => N(), 128 | } 129 | 130 | return mask.replace(token, (match) => { 131 | if (match in flags) { 132 | return flags[match]() 133 | } 134 | return match.slice(1, match.length - 1) 135 | }) 136 | } 137 | 138 | export const masks = { 139 | default: 'ddd mmm dd yyyy HH:MM:ss', 140 | shortDate: 'm/d/yy', 141 | paddedShortDate: 'mm/dd/yyyy', 142 | mediumDate: 'mmm d, yyyy', 143 | longDate: 'mmmm d, yyyy', 144 | fullDate: 'dddd, mmmm d, yyyy', 145 | shortTime: 'h:MM TT', 146 | mediumTime: 'h:MM:ss TT', 147 | longTime: 'h:MM:ss TT Z', 148 | isoDate: 'yyyy-mm-dd', 149 | isoTime: 'HH:MM:ss', 150 | isoDateTime: "yyyy-mm-dd'T'HH:MM:sso", 151 | isoUtcDateTime: "UTC:yyyy-mm-dd'T'HH:MM:ss'Z'", 152 | expiresHeaderFormat: 'ddd, dd mmm yyyy HH:MM:ss Z', 153 | } 154 | 155 | // Internationalization strings 156 | export const i18n = { 157 | dayNames: [ 158 | 'Sun', 159 | 'Mon', 160 | 'Tue', 161 | 'Wed', 162 | 'Thu', 163 | 'Fri', 164 | 'Sat', 165 | 'Sunday', 166 | 'Monday', 167 | 'Tuesday', 168 | 'Wednesday', 169 | 'Thursday', 170 | 'Friday', 171 | 'Saturday', 172 | ], 173 | monthNames: [ 174 | 'Jan', 175 | 'Feb', 176 | 'Mar', 177 | 'Apr', 178 | 'May', 179 | 'Jun', 180 | 'Jul', 181 | 'Aug', 182 | 'Sep', 183 | 'Oct', 184 | 'Nov', 185 | 'Dec', 186 | 'January', 187 | 'February', 188 | 'March', 189 | 'April', 190 | 'May', 191 | 'June', 192 | 'July', 193 | 'August', 194 | 'September', 195 | 'October', 196 | 'November', 197 | 'December', 198 | ], 199 | timeNames: ['a', 'p', 'am', 'pm', 'A', 'P', 'AM', 'PM'], 200 | } 201 | 202 | const pad = (val, len = 2) => String(val).padStart(len, '0') 203 | 204 | /** 205 | * Get day name 206 | * Yesterday, Today, Tomorrow if the date lies within, else fallback to Monday - Sunday 207 | * @param {Object} 208 | * @return {String} 209 | */ 210 | const getDayName = ({ y, m, d, _, dayName, short = false }) => { 211 | const today = new Date() 212 | const yesterday = new Date() 213 | yesterday.setDate(yesterday[_ + 'Date']() - 1) 214 | const tomorrow = new Date() 215 | tomorrow.setDate(tomorrow[_ + 'Date']() + 1) 216 | const today_d = () => today[_ + 'Date']() 217 | const today_m = () => today[_ + 'Month']() 218 | const today_y = () => today[_ + 'FullYear']() 219 | const yesterday_d = () => yesterday[_ + 'Date']() 220 | const yesterday_m = () => yesterday[_ + 'Month']() 221 | const yesterday_y = () => yesterday[_ + 'FullYear']() 222 | const tomorrow_d = () => tomorrow[_ + 'Date']() 223 | const tomorrow_m = () => tomorrow[_ + 'Month']() 224 | const tomorrow_y = () => tomorrow[_ + 'FullYear']() 225 | 226 | if (today_y() === y && today_m() === m && today_d() === d) { 227 | return short ? 'Tdy' : 'Today' 228 | } else if ( 229 | yesterday_y() === y && 230 | yesterday_m() === m && 231 | yesterday_d() === d 232 | ) { 233 | return short ? 'Ysd' : 'Yesterday' 234 | } else if (tomorrow_y() === y && tomorrow_m() === m && tomorrow_d() === d) { 235 | return short ? 'Tmw' : 'Tomorrow' 236 | } 237 | return dayName 238 | } 239 | 240 | /** 241 | * Get the ISO 8601 week number 242 | * Based on comments from 243 | * http://techblog.procurios.nl/k/n618/news/view/33796/14863/Calculate-ISO-8601-week-and-year-in-javascript.html 244 | * 245 | * @param {Date} `date` 246 | * @return {Number} 247 | */ 248 | const getWeek = (date) => { 249 | // Remove time components of date 250 | const targetThursday = new Date( 251 | date.getFullYear(), 252 | date.getMonth(), 253 | date.getDate(), 254 | ) 255 | 256 | // Change date to Thursday same week 257 | targetThursday.setDate( 258 | targetThursday.getDate() - ((targetThursday.getDay() + 6) % 7) + 3, 259 | ) 260 | 261 | // Take January 4th as it is always in week 1 (see ISO 8601) 262 | const firstThursday = new Date(targetThursday.getFullYear(), 0, 4) 263 | 264 | // Change date to Thursday same week 265 | firstThursday.setDate( 266 | firstThursday.getDate() - ((firstThursday.getDay() + 6) % 7) + 3, 267 | ) 268 | 269 | // Check if daylight-saving-time-switch occurred and correct for it 270 | const ds = 271 | targetThursday.getTimezoneOffset() - firstThursday.getTimezoneOffset() 272 | targetThursday.setHours(targetThursday.getHours() - ds) 273 | 274 | // Number of weeks between target Thursday and first Thursday 275 | const weekDiff = (targetThursday - firstThursday) / (86400000 * 7) 276 | return 1 + Math.floor(weekDiff) 277 | } 278 | 279 | /** 280 | * Get ISO-8601 numeric representation of the day of the week 281 | * 1 (for Monday) through 7 (for Sunday) 282 | * 283 | * @param {Date} `date` 284 | * @return {Number} 285 | */ 286 | const getDayOfWeek = (date) => { 287 | let dow = date.getDay() 288 | if (dow === 0) { 289 | dow = 7 290 | } 291 | return dow 292 | } 293 | 294 | /** 295 | * Get proper timezone abbreviation or timezone offset. 296 | * 297 | * This will fall back to `GMT+xxxx` if it does not recognize the 298 | * timezone within the `timezone` RegEx above. Currently only common 299 | * American and Australian timezone abbreviations are supported. 300 | * 301 | * @param {String | Date} date 302 | * @return {String} 303 | */ 304 | export const formatTimezone = (date) => { 305 | return (String(date).match(timezone) || ['']) 306 | .pop() 307 | .replace(timezoneClip, '') 308 | .replace(/GMT\+0000/g, 'UTC') 309 | } 310 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # **React Native Neat Date Picker** 2 | 3 | An easy-to-use date picker for React Native. 4 | 5 |
6 | 7 |
8 | 9 |
10 | 11 |
12 | 13 | ## **Main Features** 14 | 15 | 📲 Supports both Android and iOS devices.
16 | 👍 Provides range and single selection modes.
17 | 🕒 Utilizes modern Date object for date manipulation.
18 | 🌈 Offers color customization.
19 | ✨ Clean UI
20 | 🌐 Supports multiple languages by default: Chinese, English, Spanish, German, French, Portuguese, Malagasy, and Vietnamese. You can also add any language by yourself. 21 | 22 | ## **New Update** 23 | (v1.5.0) Supports custom languages. Improves performance and accessibility.
24 | 25 | ## **Limitation** 26 | 27 | This package is **NOT** designed for react-native-web. It can work on the web but may have issues. 28 | 29 | For Expo users, it is recommended to use SDK 45 since `react-native-modal` v13.0 is compatible with `react-native` >= 0.65. 30 | 31 | ## **Dependencies** 32 | 33 | No manual dependency installation required. 34 | 35 | ## **Installation** 36 | 37 | ```bash 38 | npm i react-native-neat-date-picker 39 | ``` 40 | 41 | ## **Example** 42 | 43 | ```javascript 44 | 45 | import { useState } from 'react' 46 | import { Button, StyleSheet, Text, View } from 'react-native' 47 | import DatePicker, { RangeOutput, SingleOutput } from 'react-native-neat-date-picker' 48 | 49 | 50 | const App = () => { 51 | const [showDatePickerSingle, setShowDatePickerSingle] = useState(false) 52 | const [showDatePickerRange, setShowDatePickerRange] = useState(false) 53 | 54 | const [date, setDate] = useState('') 55 | const [startDate, setStartDate] = useState('') 56 | const [endDate, setEndDate] = useState('') 57 | 58 | const openDatePickerSingle = () => setShowDatePickerSingle(true) 59 | const openDatePickerRange = () => setShowDatePickerRange(true) 60 | 61 | const onCancelSingle = () => { 62 | // You should close the modal here 63 | setShowDatePickerSingle(false) 64 | } 65 | 66 | const onConfirmSingle = (output: SingleOutput) => { 67 | // You should close the modal here 68 | setShowDatePickerSingle(false) 69 | 70 | // The parameter 'output' is an object containing date and dateString (for single mode). 71 | // For range mode, the output contains startDate, startDateString, endDate, and endDateString 72 | console.log(output) 73 | setDate(output.dateString ?? '') 74 | } 75 | 76 | const onCancelRange = () => { 77 | setShowDatePickerRange(false) 78 | } 79 | 80 | const onConfirmRange = (output: RangeOutput) => { 81 | setShowDatePickerRange(false) 82 | setStartDate(output.startDateString ?? '') 83 | setEndDate(output.endDateString ?? '') 84 | } 85 | 86 | return ( 87 | 88 | {/* Single Date */} 89 |