diff --git a/.eslintrc.json b/.eslintrc.json index 3179b9fb..a501c924 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -13,7 +13,6 @@ "plugin:react-hooks/recommended", "next/core-web-vitals" ], - "overrides": [], "parser": "@typescript-eslint/parser", "parserOptions": { "ecmaVersion": "latest", @@ -27,12 +26,19 @@ "version": "detect" } }, - "plugins": ["react", "@typescript-eslint", "import", "prettier", "@next/eslint-plugin-next"], + "plugins": [ + "react", + "react-refresh", + "import", + "prettier", + "@next/eslint-plugin-next", + "@typescript-eslint" + ], "rules": { "indent": "off", - "linebreak-style": ["error", "unix"], - "quotes": ["error", "double"], - "semi": ["error", "always"], + "no-console": "warn", + "quotes": "error", + "semi": "error", "import/order": [ "error", { @@ -43,12 +49,18 @@ "newlines-between": "always" } ], - "react/prop-types": "off", - "react/jsx-uses-react": "off", "react/react-in-jsx-scope": "off", + "react-refresh/only-export-components": "warn", "react-hooks/rules-of-hooks": "error", - "react-hooks/exhaustive-deps": "warn", "@typescript-eslint/no-var-requires": 0, "prettier/prettier": ["error", { "endOfLine": "auto" }, { "usePrettierrc": true }] - } + }, + "overrides": [ + { + "files": ["app/page.tsx"], + "rules": { + "no-console": "off" + } + } + ] } diff --git a/.gitignore b/.gitignore index f81e4d0d..353a6537 100644 --- a/.gitignore +++ b/.gitignore @@ -15,7 +15,7 @@ build dist .rpt2_cache .eslintcache -tsconfig.tsbuildinfo +tsconfig.rollup.tsbuildinfo # misc .DS_Store diff --git a/.husky/pre-commit b/.husky/pre-commit index d24fdfc6..d0a77842 100755 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -1,4 +1 @@ -#!/usr/bin/env sh -. "$(dirname -- "$0")/_/husky.sh" - -npx lint-staged +npx lint-staged \ No newline at end of file diff --git a/.husky/pre-push b/.husky/pre-push index 79d2763d..2f80f6ed 100755 --- a/.husky/pre-push +++ b/.husky/pre-push @@ -1,4 +1 @@ -#!/usr/bin/env sh -. "$(dirname -- "$0")/_/husky.sh" - -#npm run test +# yarn test \ No newline at end of file diff --git a/.npmignore b/.npmignore index 943f8cd7..9edb66c9 100644 --- a/.npmignore +++ b/.npmignore @@ -5,7 +5,7 @@ node_modules .idea assets .git -pages +app styles .next .rollup.cache @@ -26,4 +26,6 @@ npm-debug.log yarn-error.log tailwind.config.js postcss.config.js -tsconfig.tsbuildinfo +tsconfig.rollup.tsbuildinfo +tsconfig.base.json +tsconfig.rollup.json diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 879010fa..b58a905d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -40,7 +40,7 @@ yarn pret:fix **Using npm** ```sh -npm pret:fix +npm run pret:fix ``` ## Running playground @@ -49,7 +49,7 @@ We currently use `next.js` as server for live testing. You can run the `dev` script and open your browser to `http://localhost:8888`. -See complete `props` usage in `pages/index.js` file. +See complete `props` usage in `app/page.tsx` file. **Using yarn** @@ -60,7 +60,7 @@ yarn dev **Using npm** ```sh -npm dev +npm run dev ``` ## Before you make a Pull Request diff --git a/README.md b/README.md index 325ebfdb..9bb1273a 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,7 @@ - [Features](#features) - [Documentation](#documentation) +- [Supported versions](#-supported-versions) - [Installation](#installation) - [Simple Usage](#simple-usage) - [Theming Options](#theming-options) @@ -42,29 +43,44 @@ Go to [full documentation](https://react-tailwindcss-datepicker.vercel.app/) +## ⚠️ Supported versions + +Only **react-tailwindcss-datepicker** versions greater than or equal to **1.7.4** receive bug fixes and new features. The table below lists compatibility with the different **react** versions: + +| Version | React Version | +|----------------------------------------------------------------------------|---------------| +| [2.x](https://github.com/onesine/react-tailwindcss-datepicker/tree/v2.0.0) | 19.x | +| [1.x](https://github.com/onesine/react-tailwindcss-datepicker/tree/v1.7.3) | 17.x, 18.x | + ## Installation -⚠️ React Tailwindcss Datepicker uses Tailwind CSS 3 (with the +React Tailwindcss Datepicker uses Tailwind CSS 3 (with the [@tailwindcss/forms](https://github.com/tailwindlabs/tailwindcss-forms) plugin) & [Dayjs](https://day.js.org/en/) under the hood to work. ### Install via npm ``` -$ npm install react-tailwindcss-datepicker +npm install react-tailwindcss-datepicker ``` ### Install via yarn ``` -$ yarn add react-tailwindcss-datepicker +yarn add react-tailwindcss-datepicker +``` + +### Install for react 18 project + +``` +yarn add react-tailwindcss-datepicker@1.7.3 ``` Make sure you have installed the peer dependencies as well with the below versions. ``` -"dayjs": "^1.11.6", -"react": "^17.0.2 || ^18.2.0" +"dayjs": "^1.11.12", +"react": "^17.0.2 || ^18.2.0" || "^19.0.0" ``` ## Simple Usage @@ -87,25 +103,20 @@ module.exports = { Then use react-tailwindcss-select in your app: -```jsx -import React, { useState } from "react"; +```tsx +import { useState } from "react"; import Datepicker from "react-tailwindcss-datepicker"; const App = () => { const [value, setValue] = useState({ - startDate: new Date(), - endDate: new Date().setMonth(11) + startDate: null, + endDate: null }); - const handleValueChange = newValue => { - console.log("newValue:", newValue); - setValue(newValue); - }; - return ( -
- -
+ <> + setValue(newValue)} /> + ); }; diff --git a/app/layout.tsx b/app/layout.tsx new file mode 100644 index 00000000..551d5d0c --- /dev/null +++ b/app/layout.tsx @@ -0,0 +1,18 @@ +import { ReactNode } from "react"; +import "../styles/globals.css"; + +interface Props { + children: ReactNode; +} + +const RootLayout = (props: Props) => { + const { children } = props; + + return ( + + {children} + + ); +}; + +export default RootLayout; diff --git a/pages/index.js b/app/page.tsx similarity index 73% rename from pages/index.js rename to app/page.tsx index 5ce6034e..cdcaef8b 100644 --- a/pages/index.js +++ b/app/page.tsx @@ -1,12 +1,24 @@ -import dayjs from "dayjs"; +"use client"; + import Head from "next/head"; import { useState } from "react"; -import Datepicker from "../src"; +import Datepicker, { + ColorKeys, + DateLookingType, + DateRangeType, + DateValueType, + PopoverDirectionType, + WeekStringType +} from "../src"; import { COLORS, DATE_LOOKING_OPTIONS } from "../src/constants"; +import { dateFormat, dateIsValid } from "../src/libs/date"; + +const WEEK_DAY = ["mon", "tue", "wed", "thu", "fri", "sat", "sun"] as const; +const POPOVER_DIRECTION = ["up", "down"] as const; export default function Playground() { - const [value, setValue] = useState({ + const [value, setValue] = useState({ startDate: null, endDate: null }); @@ -26,17 +38,14 @@ export default function Playground() { const [readOnly, setReadOnly] = useState(false); const [minDate, setMinDate] = useState(""); const [maxDate, setMaxDate] = useState(""); - const [dateLooking, setDateLooking] = useState(true); - const [disabledDates, setDisabledDates] = useState([]); + const [dateLooking, setDateLooking] = useState(undefined); + const [disabledDates, setDisabledDates] = useState([]); const [newDisabledDates, setNewDisabledDates] = useState({ startDate: "", endDate: "" }); - const [startFrom, setStartFrom] = useState("2023-03-01"); - const [startWeekOn, setStartWeekOn] = useState(""); + const [startFrom, setStartFrom] = useState(dateFormat(new Date(), "YYYY-MM-DD") || ""); + const [startWeekOn, setStartWeekOn] = useState("mon"); + const [required, setRequired] = useState(false); + const [popoverDirection, setPopoverDirection] = useState("down"); - const handleChange = (value, e) => { - setValue(value); - console.log(e); - console.log("value", value); - }; return (
@@ -52,18 +61,25 @@ export default function Playground() {
{ + setValue(value); + console.log(e); + console.log("value", { + startDate: value?.startDate?.toLocaleDateString() || null, + endDate: value?.endDate?.toLocaleDateString() || null + }); + }} useRange={useRange} showFooter={showFooter} showShortcuts={showShortcuts} configs={{ shortcuts: { - today: "TText", - yesterday: "YText", - past: period => `P-${period} Text`, - currentMonth: "CMText", - pastMonth: "PMText", + today: "Today", + yesterday: "Yesterday", + past: period => `Last ${period} days`, + currentMonth: "This month", + pastMonth: "Last month", last3Days: { text: "Last 3 days", period: { @@ -94,9 +110,7 @@ export default function Playground() { asSingle={asSingle} placeholder={placeholder} separator={separator} - startFrom={ - startFrom.length && dayjs(startFrom).isValid() ? new Date(startFrom) : null - } + startFrom={dateIsValid(new Date(startFrom)) ? new Date(startFrom) : null} i18n={i18n} disabled={disabled} inputClassName={inputClassName} @@ -104,15 +118,16 @@ export default function Playground() { toggleClassName={toggleClassName} displayFormat={displayFormat} readOnly={readOnly} - minDate={minDate} - maxDate={maxDate} + minDate={dateIsValid(new Date(minDate)) ? new Date(minDate) : undefined} + maxDate={dateIsValid(new Date(maxDate)) ? new Date(maxDate) : undefined} dateLooking={dateLooking} disabledDates={disabledDates} - startWeekOn={startWeekOn} + startWeekOn={startWeekOn as WeekStringType} toggleIcon={isEmpty => { return isEmpty ? "Select Date" : "Clear"; }} - popoverDirection={"down"} + popoverDirection={popoverDirection} + required={required} // classNames={{ // input: ({ disabled, readOnly, className }) => { // if (disabled) { @@ -215,7 +230,22 @@ export default function Playground() {
+
+
+ setRequired(e.target.checked)} + /> + +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
- { - setStartWeekOn(e.target.value); + setStartWeekOn(e.target.value as WeekStringType); }} - /> + > + {WEEK_DAY.map((item, index) => ( + + ))} + +
+ +
+ + +
-
-
-

- Disable Dates -

-
+ +
+
+ +

+ Disable Dates +

+ +
+
+ { setNewDisabledDates(prev => { return { @@ -424,14 +506,18 @@ export default function Playground() { }} />
-
+ +
+ { setNewDisabledDates(prev => { return { @@ -442,44 +528,54 @@ export default function Playground() { }} />
-
- -
-
- {disabledDates.map((range, index) => ( -
- - - {range.startDate} - {range.endDate} - -
- ))} -
+
+ +
+ +
+ +
+ {disabledDates.map((range, index) => ( +
+ + + {range.startDate?.toLocaleDateString()} -{" "} + {range.endDate?.toLocaleDateString()} + +
+ ))}
+
// NOTE: This file should not be edited -// see https://nextjs.org/docs/basic-features/typescript for more information. +// see https://nextjs.org/docs/app/api-reference/config/typescript for more information. diff --git a/next.config.js b/next.config.js deleted file mode 100644 index eda88d1a..00000000 --- a/next.config.js +++ /dev/null @@ -1,6 +0,0 @@ -/** @type {import('next').NextConfig} */ -const nextConfig = { - reactStrictMode: true -}; - -module.exports = nextConfig; diff --git a/next.config.mjs b/next.config.mjs new file mode 100644 index 00000000..4678774e --- /dev/null +++ b/next.config.mjs @@ -0,0 +1,4 @@ +/** @type {import('next').NextConfig} */ +const nextConfig = {}; + +export default nextConfig; diff --git a/package.json b/package.json index ac9c06cf..870c5241 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "react-tailwindcss-datepicker", - "version": "1.6.6", + "version": "2.0.0", "description": "A modern React Datepicker using Tailwind CSS 3", "main": "dist/index.cjs.js", "module": "dist/index.esm.js", @@ -9,17 +9,15 @@ "license": "MIT", "scripts": { "watch": "rollup -c -w", - "clean": "rm -rf dist", - "lint": "eslint --ignore-path .gitignore .", - "lint:fix": "eslint --ignore-path .gitignore --fix .", - "pret": "prettier -c .", - "pret:fix": "prettier --ignore-path .gitignore --config ./.prettierrc --write './**/*.{js,jsx,ts,tsx,css,md,json}'", - "code-style": "npm run pret && npm run lint", - "code-style:fix": "npm run pret:fix && npm run pret:fix", - "build": "npm run code-style && npm run clean && rollup -c", - "pub": "npm run build && npm publish", + "clean": "rm -rf dist .rollup.cache tsconfig.rollup.tsbuildinfo", + "lint": "eslint .", + "lint:fix": "eslint --fix .", + "pret:fix": "prettier --write .", + "format": "npm run pret:fix && npm run lint:fix", + "build": "npm run lint && npm run clean && rollup -c rollup.config.js --bundleConfigAsCjs", + "pub": "npm run build && np --no-tests", "dev": "next dev -p 8888", - "prepare": "husky install" + "prepare": "husky" }, "repository": { "type": "git", @@ -38,45 +36,51 @@ "tailwind-daterange-picker" ], "peerDependencies": { - "dayjs": "^1.11.6", - "react": "^17.0.2 || ^18.2.0" + "dayjs": "^1.11.12", + "react": "^17.0.2 || ^18.2.0 || ^19.0.0" }, "devDependencies": { - "@rollup/plugin-commonjs": "^24.0.1", - "@rollup/plugin-node-resolve": "^15.0.1", - "@rollup/plugin-typescript": "^11.0.0", - "@tailwindcss/forms": "^0.5.3", - "@types/node": "18.14.5", - "@types/react": "^18.0.21", - "@typescript-eslint/eslint-plugin": "^5.45.0", - "@typescript-eslint/parser": "^5.45.0", - "autoprefixer": "^10.4.13", - "dayjs": "^1.11.7", - "eslint": "^8.29.0", - "eslint-config-next": "^13.1.1", - "eslint-config-prettier": "^8.5.0", - "eslint-plugin-import": "^2.26.0", - "eslint-plugin-prettier": "^4.2.1", - "eslint-plugin-react": "^7.31.11", - "eslint-plugin-react-hooks": "^4.6.0", - "husky": "^8.0.0", - "lint-staged": "^13.2.3", - "next": "^13.1.1", - "postcss": "^8.4.19", - "prettier": "^2.8.0", - "react": "^18.2.0", - "react-dom": "^18.2.0", - "rollup": "^2.77.2", - "tailwindcss": "^3.2.4", - "tslib": "^2.4.0", - "typescript": "^4.8.4" + "@rollup/plugin-commonjs": "^28.0.2", + "@rollup/plugin-node-resolve": "^16.0.0", + "@rollup/plugin-typescript": "^12.1.2", + "@tailwindcss/forms": "^0.5.7", + "@types/node": "^22.3.0", + "@types/react": "^19.0.8", + "@typescript-eslint/eslint-plugin": "^8.22.0", + "@typescript-eslint/parser": "^8.22.0", + "autoprefixer": "^10.4.20", + "dayjs": "^1.11.12", + "eslint": "^8.57.0", + "eslint-config-next": "^15.1.6", + "eslint-config-prettier": "^9.1.0", + "eslint-plugin-import": "^2.29.1", + "eslint-plugin-prettier": "^5.2.1", + "eslint-plugin-react": "^7.35.0", + "eslint-plugin-react-hooks": "^5.1.0", + "eslint-plugin-react-refresh": "^0.4.18", + "husky": "^9.1.4", + "lint-staged": "^15.2.9", + "next": "^15.1.6", + "pinst": "^3.0.0", + "postcss": "^8.4.41", + "prettier": "^3.3.3", + "react": "^19.0.0", + "react-dom": "^19.0.0", + "rollup": "^4.34.0", + "tailwindcss": "^3.4.10", + "tslib": "^2.8.1", + "typescript": "^5.5.4" }, "lint-staged": { "*.{ts,tsx}": [ - "npm run lint" + "eslint", + "prettier --write" ], - "*.{ts,tsx,css,scss,md}": [ - "npm run pret:fix" + "*.{css,scss,json,md}": [ + "prettier --write" ] - } + }, + "files": [ + "dist" + ] } diff --git a/pages/_app.js b/pages/_app.js deleted file mode 100644 index c9bee70c..00000000 --- a/pages/_app.js +++ /dev/null @@ -1,7 +0,0 @@ -import "../styles/globals.css"; - -function MyApp({ Component, pageProps }) { - return ; -} - -export default MyApp; diff --git a/rollup.config.js b/rollup.config.js index 8217c424..ad9af56f 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -2,27 +2,22 @@ import commonjs from "@rollup/plugin-commonjs"; import resolve from "@rollup/plugin-node-resolve"; import typescript from "@rollup/plugin-typescript"; -const packageJson = require("./package.json"); -const options = require("./tsconfig.json"); - module.exports = { input: "src/index.tsx", output: [ { - file: packageJson.main, + file: "dist/index.cjs.js", format: "cjs", - exports: "auto", sourcemap: true, inlineDynamicImports: true }, { - file: packageJson.module, + file: "dist/index.esm.js", format: "esm", - exports: "auto", sourcemap: true, inlineDynamicImports: true } ], external: ["react", "dayjs"], - plugins: [resolve(), commonjs(), typescript({ ...options.compilerOptions, jsx: "react" })] + plugins: [resolve(), commonjs(), typescript({ tsconfig: "./tsconfig.rollup.json" })] }; diff --git a/src/components/Calendar/Days.tsx b/src/components/Calendar/Days.tsx index 43f0b9b6..10a0dbe6 100644 --- a/src/components/Calendar/Days.tsx +++ b/src/components/Calendar/Days.tsx @@ -1,34 +1,34 @@ -import dayjs from "dayjs"; -import isBetween from "dayjs/plugin/isBetween"; -import React, { useCallback, useContext } from "react"; +import { useCallback, useContext } from "react"; import { BG_COLOR, TEXT_COLOR } from "../../constants"; import DatepickerContext from "../../contexts/DatepickerContext"; -import { formatDate, nextMonth, previousMonth, classNames as cn } from "../../helpers"; +import { classNames as cn } from "../../helpers"; +import { + dateIsAfter, + dateIsBefore, + dateIsBetween, + dateIsSame, + dateIsSameOrAfter, + dateIsSameOrBefore, + isCurrentDay +} from "../../libs/date"; import { Period } from "../../types"; -dayjs.extend(isBetween); - interface Props { - calendarData: { - date: dayjs.Dayjs; - days: { - previous: number[]; - current: number[]; - next: number[]; - }; + days: { + previous: Date[]; + current: Date[]; + next: Date[]; }; - onClickPreviousDays: (day: number) => void; - onClickDay: (day: number) => void; - onClickNextDays: (day: number) => void; + onClickPreviousDays: (day: Date) => void; + onClickDay: (day: Date) => void; + onClickNextDays: (day: Date) => void; } -const Days: React.FC = ({ - calendarData, - onClickPreviousDays, - onClickDay, - onClickNextDays -}) => { +const Days = (props: Props) => { + // Props + const { days, onClickPreviousDays, onClickDay, onClickNextDays } = props; + // Contexts const { primaryColor, @@ -43,55 +43,50 @@ const Days: React.FC = ({ // Functions const currentDateClass = useCallback( - (item: number) => { - const itemDate = `${calendarData.date.year()}-${calendarData.date.month() + 1}-${ - item >= 10 ? item : "0" + item - }`; - if (formatDate(dayjs()) === formatDate(dayjs(itemDate))) + (day: Date) => { + if (isCurrentDay(day)) return TEXT_COLOR["500"][primaryColor as keyof (typeof TEXT_COLOR)["500"]]; return ""; }, - [calendarData.date, primaryColor] + [primaryColor] ); const activeDateData = useCallback( - (day: number) => { - const fullDay = `${calendarData.date.year()}-${calendarData.date.month() + 1}-${day}`; + (day: Date) => { let className = ""; - if (dayjs(fullDay).isSame(period.start) && dayjs(fullDay).isSame(period.end)) { + const dayIsSameStart = period.start && dateIsSame(day, period.start, "date"); + const dayIsSameEnd = period.end && dateIsSame(day, period.end, "date"); + const dayIsSameHoverDay = dayHover && dateIsSame(day, dayHover, "date"); + + if (dayIsSameStart && dayIsSameEnd) { className = ` ${BG_COLOR["500"][primaryColor]} text-white font-medium rounded-full`; - } else if (dayjs(fullDay).isSame(period.start)) { + } else if (dayIsSameStart) { className = ` ${BG_COLOR["500"][primaryColor]} text-white font-medium ${ - dayjs(fullDay).isSame(dayHover) && !period.end - ? "rounded-full" - : "rounded-l-full" + dayIsSameHoverDay && !period.end ? "rounded-full" : "rounded-l-full" }`; - } else if (dayjs(fullDay).isSame(period.end)) { + } else if (dayIsSameEnd) { className = ` ${BG_COLOR["500"][primaryColor]} text-white font-medium ${ - dayjs(fullDay).isSame(dayHover) && !period.start - ? "rounded-full" - : "rounded-r-full" + dayIsSameHoverDay && !period.start ? "rounded-full" : "rounded-r-full" }`; } return { - active: dayjs(fullDay).isSame(period.start) || dayjs(fullDay).isSame(period.end), + active: dayIsSameStart || dayIsSameEnd, className: className }; }, - [calendarData.date, dayHover, period.end, period.start, primaryColor] + [dayHover, period.end, period.start, primaryColor] ); const hoverClassByDay = useCallback( - (day: number) => { + (day: Date) => { let className = currentDateClass(day); - const fullDay = `${calendarData.date.year()}-${calendarData.date.month() + 1}-${ - day >= 10 ? day : "0" + day - }`; if (period.start && period.end) { - if (dayjs(fullDay).isBetween(period.start, period.end, "day", "[)")) { + if ( + dateIsBetween(day, period.start, period.end, "day", { start: true, end: false }) + ) { return ` ${BG_COLOR["100"][primaryColor]} ${currentDateClass( day )} dark:bg-white/10`; @@ -102,19 +97,25 @@ const Days: React.FC = ({ return className; } - if (period.start && dayjs(fullDay).isBetween(period.start, dayHover, "day", "[)")) { + if ( + period.start && + dateIsBetween(day, period.start, dayHover, "day", { start: true, end: false }) + ) { className = ` ${BG_COLOR["100"][primaryColor]} ${currentDateClass( day )} dark:bg-white/10`; } - if (period.end && dayjs(fullDay).isBetween(dayHover, period.end, "day", "[)")) { + if ( + period.end && + dateIsBetween(day, dayHover, period.end, "day", { start: true, end: false }) + ) { className = ` ${BG_COLOR["100"][primaryColor]} ${currentDateClass( day )} dark:bg-white/10`; } - if (dayHover === fullDay) { + if (dateIsSame(dayHover, day, "date")) { const bgColor = BG_COLOR["500"][primaryColor]; className = ` transition-all duration-500 text-white font-medium ${bgColor} ${ period.start ? "rounded-r-full" : "rounded-l-full" @@ -123,61 +124,32 @@ const Days: React.FC = ({ return className; }, - [calendarData.date, currentDateClass, dayHover, period.end, period.start, primaryColor] + [currentDateClass, dayHover, period.end, period.start, primaryColor] ); const isDateTooEarly = useCallback( - (day: number, type: "current" | "previous" | "next") => { - if (!minDate) { - return false; - } - const object = { - previous: previousMonth(calendarData.date), - current: calendarData.date, - next: nextMonth(calendarData.date) - }; - const newDate = object[type as keyof typeof object]; - const formattedDate = newDate.set("date", day); - return dayjs(formattedDate).isSame(dayjs(minDate), "day") - ? false - : dayjs(formattedDate).isBefore(dayjs(minDate)); + (day: Date) => { + if (!minDate) return false; + + return dateIsBefore(day, minDate, "date"); }, - [calendarData.date, minDate] + [minDate] ); const isDateTooLate = useCallback( - (day: number, type: "current" | "previous" | "next") => { - if (!maxDate) { - return false; - } - const object = { - previous: previousMonth(calendarData.date), - current: calendarData.date, - next: nextMonth(calendarData.date) - }; - const newDate = object[type as keyof typeof object]; - const formattedDate = newDate.set("date", day); - return dayjs(formattedDate).isSame(dayjs(maxDate), "day") - ? false - : dayjs(formattedDate).isAfter(dayjs(maxDate)); + (day: Date) => { + if (!maxDate) return false; + + return dateIsAfter(day, maxDate, "date"); }, - [calendarData.date, maxDate] + [maxDate] ); const isDateDisabled = useCallback( - (day: number, type: "current" | "previous" | "next") => { - if (isDateTooEarly(day, type) || isDateTooLate(day, type)) { + (day: Date) => { + if (isDateTooEarly(day) || isDateTooLate(day)) { return true; } - const object = { - previous: previousMonth(calendarData.date), - current: calendarData.date, - next: nextMonth(calendarData.date) - }; - const newDate = object[type as keyof typeof object]; - const formattedDate = `${newDate.year()}-${newDate.month() + 1}-${ - day >= 10 ? day : "0" + day - }`; if (!disabledDates || (Array.isArray(disabledDates) && !disabledDates.length)) { return false; @@ -186,25 +158,24 @@ const Days: React.FC = ({ let matchingCount = 0; disabledDates?.forEach(dateRange => { if ( - dayjs(formattedDate).isAfter(dateRange.startDate) && - dayjs(formattedDate).isBefore(dateRange.endDate) - ) { - matchingCount++; - } - if ( - dayjs(formattedDate).isSame(dateRange.startDate) || - dayjs(formattedDate).isSame(dateRange.endDate) + dateRange.startDate && + dateRange.endDate && + dateIsBetween(day, dateRange.startDate, dateRange.endDate, "date", { + start: true, + end: true + }) ) { matchingCount++; } }); + return matchingCount > 0; }, - [calendarData.date, isDateTooEarly, isDateTooLate, disabledDates] + [isDateTooEarly, isDateTooLate, disabledDates] ); const buttonClass = useCallback( - (day: number, type: "current" | "next" | "previous") => { + (day: Date, type: "current" | "next" | "previous") => { const baseClass = "flex items-center justify-center w-12 h-12 lg:w-10 lg:h-10"; if (type === "current") { return cn( @@ -212,10 +183,10 @@ const Days: React.FC = ({ !activeDateData(day).active ? hoverClassByDay(day) : activeDateData(day).className, - isDateDisabled(day, type) && "line-through" + isDateDisabled(day) && "line-through" ); } - return cn(baseClass, isDateDisabled(day, type) && "line-through", "text-gray-400"); + return cn(baseClass, isDateDisabled(day) && "line-through", "text-gray-400"); }, [activeDateData, hoverClassByDay, isDateDisabled] ); @@ -225,10 +196,11 @@ const Days: React.FC = ({ if (!Array.isArray(disabledDates)) { return false; } + for (let i = 0; i < disabledDates.length; i++) { if ( - dayjs(hoverPeriod.start).isBefore(disabledDates[i].startDate) && - dayjs(hoverPeriod.end).isAfter(disabledDates[i].endDate) + dateIsSameOrBefore(hoverPeriod.start, disabledDates[i].startDate, "date") && + dateIsSameOrAfter(hoverPeriod.end, disabledDates[i].endDate, "date") ) { return true; } @@ -238,27 +210,15 @@ const Days: React.FC = ({ [disabledDates] ); - const getMetaData = useCallback(() => { - return { - previous: previousMonth(calendarData.date), - current: calendarData.date, - next: nextMonth(calendarData.date) - }; - }, [calendarData.date]); - const hoverDay = useCallback( - (day: number, type: string) => { - const object = getMetaData(); - const newDate = object[type as keyof typeof object]; - const newHover = `${newDate.year()}-${newDate.month() + 1}-${ - day >= 10 ? day : "0" + day - }`; - + (day: Date) => { if (period.start && !period.end) { - const hoverPeriod = { ...period, end: newHover }; - if (dayjs(newHover).isBefore(dayjs(period.start))) { - hoverPeriod.start = newHover; + const hoverPeriod = { ...period, end: day }; + + if (dateIsBefore(day, period.start, "date")) { + hoverPeriod.start = day; hoverPeriod.end = period.start; + if (!checkIfHoverPeriodContainsDisabledPeriod(hoverPeriod)) { changePeriod({ start: null, @@ -266,16 +226,18 @@ const Days: React.FC = ({ }); } } + if (!checkIfHoverPeriodContainsDisabledPeriod(hoverPeriod)) { - changeDayHover(newHover); + changeDayHover(day); } } if (!period.start && period.end) { - const hoverPeriod = { ...period, start: newHover }; - if (dayjs(newHover).isAfter(dayjs(period.end))) { + const hoverPeriod = { ...period, start: day }; + + if (dateIsAfter(day, period.end, "date")) { hoverPeriod.start = period.end; - hoverPeriod.end = newHover; + hoverPeriod.end = day; if (!checkIfHoverPeriodContainsDisabledPeriod(hoverPeriod)) { changePeriod({ start: period.end, @@ -284,21 +246,15 @@ const Days: React.FC = ({ } } if (!checkIfHoverPeriodContainsDisabledPeriod(hoverPeriod)) { - changeDayHover(newHover); + changeDayHover(day); } } }, - [ - changeDayHover, - changePeriod, - checkIfHoverPeriodContainsDisabledPeriod, - getMetaData, - period - ] + [changeDayHover, changePeriod, checkIfHoverPeriodContainsDisabledPeriod, period] ); const handleClickDay = useCallback( - (day: number, type: "previous" | "current" | "next") => { + (day: Date, type: "previous" | "current" | "next") => { function continueClick() { if (type === "previous") { onClickPreviousDays(day); @@ -314,16 +270,12 @@ const Days: React.FC = ({ } if (disabledDates?.length) { - const object = getMetaData(); - const newDate = object[type as keyof typeof object]; - const clickDay = `${newDate.year()}-${newDate.month() + 1}-${ - day >= 10 ? day : "0" + day - }`; + const daySelectedIsSameHoverDay = dayHover && dateIsSame(day, dayHover, "date"); - if (period.start && !period.end) { - dayjs(clickDay).isSame(dayHover) && continueClick(); - } else if (!period.start && period.end) { - dayjs(clickDay).isSame(dayHover) && continueClick(); + if (period.start && !period.end && daySelectedIsSameHoverDay) { + continueClick(); + } else if (!period.start && period.end && daySelectedIsSameHoverDay) { + continueClick(); } else { continueClick(); } @@ -334,7 +286,6 @@ const Days: React.FC = ({ [ dayHover, disabledDates?.length, - getMetaData, onClickDay, onClickNextDays, onClickPreviousDays, @@ -345,48 +296,48 @@ const Days: React.FC = ({ return (
- {calendarData.days.previous.map((item, index) => ( + {days.previous.map((item, index) => ( ))} - {calendarData.days.current.map((item, index) => ( + {days.current.map((item, index) => ( ))} - {calendarData.days.next.map((item, index) => ( + {days.next.map((item, index) => ( ))}
diff --git a/src/components/Calendar/Months.tsx b/src/components/Calendar/Months.tsx index ebd1ce94..0ec2aba5 100644 --- a/src/components/Calendar/Months.tsx +++ b/src/components/Calendar/Months.tsx @@ -1,19 +1,24 @@ -import dayjs from "dayjs"; -import React, { useContext } from "react"; +import { useContext, useEffect } from "react"; import { MONTHS } from "../../constants"; import DatepickerContext from "../../contexts/DatepickerContext"; -import { loadLanguageModule } from "../../helpers"; -import { RoundedButton } from "../utils"; +import { dateFormat, loadLanguageModule } from "../../libs/date"; +import RoundedButton from "../RoundedButton"; interface Props { currentMonth: number; clickMonth: (month: number) => void; } -const Months: React.FC = ({ currentMonth, clickMonth }) => { +const Months = (props: Props) => { + const { currentMonth, clickMonth } = props; + const { i18n } = useContext(DatepickerContext); - loadLanguageModule(i18n); + + useEffect(() => { + loadLanguageModule(i18n); + }, [i18n]); + return (
{MONTHS.map(item => ( @@ -25,7 +30,7 @@ const Months: React.FC = ({ currentMonth, clickMonth }) => { }} active={currentMonth === item} > - <>{dayjs(`2022-${item}-01`).locale(i18n).format("MMM")} + {dateFormat(new Date(2022, item - 1, 1), "MMM", i18n)} ))}
diff --git a/src/components/Calendar/Week.tsx b/src/components/Calendar/Week.tsx index 45e957c2..bcb452ff 100644 --- a/src/components/Calendar/Week.tsx +++ b/src/components/Calendar/Week.tsx @@ -1,13 +1,17 @@ -import dayjs from "dayjs"; -import React, { useContext, useMemo } from "react"; +import { useContext, useEffect, useMemo } from "react"; import { DAYS } from "../../constants"; import DatepickerContext from "../../contexts/DatepickerContext"; -import { loadLanguageModule, shortString, ucFirst } from "../../helpers"; +import { shortString, ucFirst } from "../../helpers"; +import { dateFormat, loadLanguageModule } from "../../libs/date"; -const Week: React.FC = () => { +const Week = () => { const { i18n, startWeekOn } = useContext(DatepickerContext); - loadLanguageModule(i18n); + + useEffect(() => { + loadLanguageModule(i18n); + }, [i18n]); + const startDateModifier = useMemo(() => { if (startWeekOn) { switch (startWeekOn) { @@ -38,9 +42,11 @@ const Week: React.FC = () => {
{ucFirst( shortString( - dayjs(`2022-11-${6 + (item + startDateModifier)}`) - .locale(i18n) - .format("ddd") + dateFormat( + new Date(2022, 10, 6 + item + startDateModifier), + "ddd", + i18n + ) || "" ) )}
diff --git a/src/components/Calendar/Years.tsx b/src/components/Calendar/Years.tsx index 0bbef7b5..5980bb25 100644 --- a/src/components/Calendar/Years.tsx +++ b/src/components/Calendar/Years.tsx @@ -1,7 +1,7 @@ -import React, { useContext } from "react"; +import { useContext, useMemo } from "react"; import { generateArrayNumber } from "../../helpers"; -import { RoundedButton } from "../utils"; +import RoundedButton from "../RoundedButton"; import DatepickerContext from "contexts/DatepickerContext"; @@ -13,31 +13,40 @@ interface Props { clickYear: (data: number) => void; } -const Years: React.FC = ({ year, currentYear, minYear, maxYear, clickYear }) => { +const Years = (props: Props) => { + const { year, currentYear, minYear, maxYear, clickYear } = props; + const { dateLooking } = useContext(DatepickerContext); - let startDate = 0; - let endDate = 0; - - switch (dateLooking) { - case "backward": - startDate = year - 11; - endDate = year; - break; - case "middle": - startDate = year - 4; - endDate = year + 7; - break; - case "forward": - default: - startDate = year; - endDate = year + 11; - break; - } + const date = useMemo(() => { + let start: number; + let end: number; + + switch (dateLooking) { + case "backward": + start = year - 11; + end = year; + break; + case "middle": + start = year - 4; + end = year + 7; + break; + case "forward": + default: + start = year; + end = year + 11; + break; + } + + return { + start, + end + }; + }, [dateLooking, year]); return (
- {generateArrayNumber(startDate, endDate).map((item, index) => ( + {generateArrayNumber(date.start, date.end).map((item, index) => ( void; onClickNext: () => void; changeMonth: (month: number) => void; changeYear: (year: number) => void; } -const Calendar: React.FC = ({ - date, - minDate, - maxDate, - onClickPrevious, - onClickNext, - changeMonth, - changeYear -}) => { +const Calendar = (props: Props) => { + // Props + const { date, minDate, maxDate, onClickPrevious, onClickNext, changeMonth, changeYear } = props; + // Contexts const { period, @@ -66,32 +64,15 @@ const Calendar: React.FC = ({ // States const [showMonths, setShowMonths] = useState(false); const [showYears, setShowYears] = useState(false); - const [year, setYear] = useState(date.year()); - // Functions - const previous = useCallback(() => { - return getLastDaysInMonth( - previousMonth(date), - getNumberOfDay(getFirstDayInMonth(date).ddd, startWeekOn) - ); - }, [date, startWeekOn]); - - const current = useCallback(() => { - return getDaysInMonth(formatDate(date)); - }, [date]); - - const next = useCallback(() => { - return getFirstDaysInMonth( - previousMonth(date), - CALENDAR_SIZE - (previous().length + current().length) - ); - }, [current, date, previous]); + const [year, setYear] = useState(date.getFullYear()); + // Functions const hideMonths = useCallback(() => { - showMonths && setShowMonths(false); + if (showMonths) setShowMonths(false); }, [showMonths]); const hideYears = useCallback(() => { - showYears && setShowYears(false); + if (showYears) setShowYears(false); }, [showYears]); const clickMonth = useCallback( @@ -115,27 +96,25 @@ const Calendar: React.FC = ({ ); const clickDay = useCallback( - (day: number, month = date.month() + 1, year = date.year()) => { - const fullDay = `${year}-${month}-${day}`; + (day: Date, after?: () => void) => { let newStart; let newEnd = null; - function chosePeriod(start: string, end: string) { - const ipt = input?.current; + function chosePeriod(start: Date, end: Date) { changeDatepickerValue( { - startDate: dayjs(start).format(DATE_FORMAT), - endDate: dayjs(end).format(DATE_FORMAT) + startDate: start, + endDate: end }, - ipt + input ); + hideDatepicker(); } if (period.start && period.end) { - if (changeDayHover) { - changeDayHover(null); - } + changeDayHover(null); + changePeriod({ start: null, end: null @@ -144,30 +123,30 @@ const Calendar: React.FC = ({ if ((!period.start && !period.end) || (period.start && period.end)) { if (!period.start && !period.end) { - changeDayHover(fullDay); + changeDayHover(day); } - newStart = fullDay; + + newStart = day; + if (asSingle) { - newEnd = fullDay; - chosePeriod(fullDay, fullDay); + newEnd = day; + if (!showFooter) { + chosePeriod(day, day); + } } } else { if (period.start && !period.end) { // start not null // end null - const condition = - dayjs(fullDay).isSame(dayjs(period.start)) || - dayjs(fullDay).isAfter(dayjs(period.start)); - newStart = condition ? period.start : fullDay; - newEnd = condition ? fullDay : period.start; + const condition = dateIsSameOrAfter(day, period.start, "date"); + newStart = condition ? period.start : day; + newEnd = condition ? day : period.start; } else { // Start null // End not null - const condition = - dayjs(fullDay).isSame(dayjs(period.end)) || - dayjs(fullDay).isBefore(dayjs(period.end)); - newStart = condition ? fullDay : period.start; - newEnd = condition ? period.end : fullDay; + const condition = dateIsSameOrBefore(day, period.end, "date"); + newStart = condition ? day : period.start; + newEnd = condition ? period.end : day; } if (!showFooter) { @@ -183,13 +162,18 @@ const Calendar: React.FC = ({ end: newEnd }); } + + if (after) { + setTimeout(() => { + after(); + }, 50); + } }, [ asSingle, changeDatepickerValue, changeDayHover, changePeriod, - date, hideDatepicker, period.end, period.start, @@ -199,47 +183,60 @@ const Calendar: React.FC = ({ ); const clickPreviousDays = useCallback( - (day: number) => { - const newDate = previousMonth(date); - clickDay(day, newDate.month() + 1, newDate.year()); - onClickPrevious(); + (day: Date) => { + clickDay(day, () => { + onClickPrevious(); + }); }, - [clickDay, date, onClickPrevious] + [clickDay, onClickPrevious] ); const clickNextDays = useCallback( - (day: number) => { - const newDate = nextMonth(date); - clickDay(day, newDate.month() + 1, newDate.year()); - onClickNext(); + (day: Date) => { + clickDay(day, () => { + onClickNext(); + }); }, - [clickDay, date, onClickNext] + [clickDay, onClickNext] ); // UseEffects & UseLayoutEffect useEffect(() => { - setYear(date.year()); + setYear(date.getFullYear()); }, [date]); // Variables const calendarData = useMemo(() => { + const firstDateCurrentMonth = firstDayOfMonth(date); + const lastDateCurrentMonth = endDayOfMonth(date); + + const startWeekOnIndex = weekDayStringToIndex(startWeekOn || START_WEEK); + + const previous = previousDaysInWeek(firstDateCurrentMonth, startWeekOnIndex); + const current = allDaysInMonth(date); + const next = nextDaysInWeek(lastDateCurrentMonth, startWeekOnIndex); + + const remainingDaysLength = + CALENDAR_SIZE - (previous.length + current.length + next.length); + + if (remainingDaysLength > 0) { + const lastNextDate = next[next.length - 1] || current[current.length - 1]; + next.push(...getNextDates(lastNextDate, remainingDaysLength)); + } + return { - date: date, - days: { - previous: previous(), - current: current(), - next: next() - } + previous: previous, + current: current, + next: next }; - }, [current, date, next, previous]); - const minYear = React.useMemo( - () => (minDate && dayjs(minDate).isValid() ? dayjs(minDate).year() : null), - [minDate] - ); - const maxYear = React.useMemo( - () => (maxDate && dayjs(maxDate).isValid() ? dayjs(maxDate).year() : null), - [maxDate] - ); + }, [date, startWeekOn]); + + const years = useMemo(() => { + return { + min: minDate && dateIsValid(minDate) ? minDate.getFullYear() : null, + max: maxDate && dateIsValid(maxDate) ? maxDate.getFullYear() : null + }; + }, [maxDate, minDate]); return (
@@ -257,7 +254,7 @@ const Calendar: React.FC = ({ { - setYear(year - 12); + setYear(year - NUMBER_YEARS_SHOW); }} > @@ -273,7 +270,7 @@ const Calendar: React.FC = ({ hideYears(); }} > - <>{calendarData.date.locale(i18n).format("MMM")} + {dateFormat(date, "MMM", i18n)}
@@ -284,7 +281,7 @@ const Calendar: React.FC = ({ hideMonths(); }} > - <>{calendarData.date.year()} + <>{date.getFullYear()}
@@ -294,7 +291,7 @@ const Calendar: React.FC = ({ { - setYear(year + 12); + setYear(year + NUMBER_YEARS_SHOW); }} > @@ -313,15 +310,15 @@ const Calendar: React.FC = ({
{showMonths && ( - + )} {showYears && ( )} @@ -331,7 +328,7 @@ const Calendar: React.FC = ({ = ({ - primaryColor = "blue", - value = null, - onChange, - useRange = true, - showFooter = false, - showShortcuts = false, - configs = undefined, - asSingle = false, - placeholder = null, - separator = "~", - startFrom = null, - i18n = LANGUAGE, - disabled = false, - inputClassName = null, - containerClassName = null, - toggleClassName = null, - toggleIcon = undefined, - displayFormat = DATE_FORMAT, - readOnly = false, - minDate = null, - maxDate = null, - dateLooking = "forward", - disabledDates = null, - inputId, - inputName, - startWeekOn = "sun", - classNames = undefined, - popoverDirection = undefined -}) => { - // Ref +import { + dateFormat, + dateIsAfter, + dateIsSameOrAfter, + dateIsSameOrBefore, + dateIsValid, + dateUpdateMonth, + dateUpdateYear, + firstDayOfMonth, + nextMonthBy, + previousMonthBy +} from "../libs/date"; +import { Period, DatepickerType, ColorKeys, DateType } from "../types"; + +import Arrow from "./icons/Arrow"; +import VerticalDash from "./VerticalDash"; + +const Datepicker = (props: DatepickerType) => { + // Props + const { + asSingle = false, + + classNames = undefined, + configs = undefined, + containerClassName = null, + + dateLooking = DEFAULT_DATE_LOOKING, + disabledDates = null, + disabled = false, + displayFormat = DATE_FORMAT, + + i18n = LANGUAGE, + inputClassName = null, + inputId, + inputName, + + minDate = undefined, + maxDate = undefined, + + onChange, + + placeholder = null, + popupClassName = null, + popoverDirection = undefined, + primaryColor = DEFAULT_COLOR, + + separator = DEFAULT_SEPARATOR, + showFooter = false, + showShortcuts = false, + startFrom = null, + startWeekOn = START_WEEK, + + readOnly = false, + required = false, + + toggleClassName = null, + toggleIcon = undefined, + + useRange = true, + value = null + } = props; + + // Refs const containerRef = useRef(null); const calendarContainerRef = useRef(null); const arrowRef = useRef(null); - // State - const [firstDate, setFirstDate] = useState( - startFrom && dayjs(startFrom).isValid() ? dayjs(startFrom) : dayjs() + // States + const [firstDate, setFirstDate] = useState( + startFrom && dateIsValid(startFrom) ? startFrom : new Date() ); - const [secondDate, setSecondDate] = useState(nextMonth(firstDate)); + const [secondDate, setSecondDate] = useState(nextMonthBy(firstDate)); const [period, setPeriod] = useState({ start: null, end: null }); - const [dayHover, setDayHover] = useState(null); + const [dayHover, setDayHover] = useState(null); const [inputText, setInputText] = useState(""); - const [inputRef, setInputRef] = useState(React.createRef()); + const [input, setInput] = useState(null); // Custom Hooks use - useOnClickOutside(containerRef, () => { + useOnClickOutside(containerRef.current, () => { const container = containerRef.current; if (container) { hideDatepicker(); @@ -95,11 +129,9 @@ const Datepicker: React.FC = ({ /* Start First */ const firstGotoDate = useCallback( - (date: dayjs.Dayjs) => { - const newDate = dayjs(formatDate(date)); - const reformatDate = dayjs(formatDate(secondDate)); - if (newDate.isSame(reformatDate) || newDate.isAfter(reformatDate)) { - setSecondDate(nextMonth(date)); + (date: Date) => { + if (dateIsSameOrAfter(date, secondDate, "date")) { + setSecondDate(nextMonthBy(date)); } setFirstDate(date); }, @@ -107,23 +139,23 @@ const Datepicker: React.FC = ({ ); const previousMonthFirst = useCallback(() => { - setFirstDate(previousMonth(firstDate)); + setFirstDate(previousMonthBy(firstDate)); }, [firstDate]); const nextMonthFirst = useCallback(() => { - firstGotoDate(nextMonth(firstDate)); + firstGotoDate(nextMonthBy(firstDate)); }, [firstDate, firstGotoDate]); const changeFirstMonth = useCallback( (month: number) => { - firstGotoDate(dayjs(`${firstDate.year()}-${month < 10 ? "0" : ""}${month}-01`)); + firstGotoDate(dateUpdateMonth(firstDate, month - 1)); }, [firstDate, firstGotoDate] ); const changeFirstYear = useCallback( (year: number) => { - firstGotoDate(dayjs(`${year}-${firstDate.month() + 1}-01`)); + firstGotoDate(dateUpdateYear(firstDate, year)); }, [firstDate, firstGotoDate] ); @@ -131,35 +163,34 @@ const Datepicker: React.FC = ({ /* Start Second */ const secondGotoDate = useCallback( - (date: dayjs.Dayjs) => { - const newDate = dayjs(formatDate(date, displayFormat)); - const reformatDate = dayjs(formatDate(firstDate, displayFormat)); - if (newDate.isSame(reformatDate) || newDate.isBefore(reformatDate)) { - setFirstDate(previousMonth(date)); + (date: Date) => { + dateIsSameOrBefore(date, firstDate, "date"); + if (dateIsSameOrBefore(date, firstDate, "date")) { + setFirstDate(previousMonthBy(date)); } setSecondDate(date); }, - [firstDate, displayFormat] + [firstDate] ); const previousMonthSecond = useCallback(() => { - secondGotoDate(previousMonth(secondDate)); + secondGotoDate(previousMonthBy(secondDate)); }, [secondDate, secondGotoDate]); const nextMonthSecond = useCallback(() => { - setSecondDate(nextMonth(secondDate)); + setSecondDate(nextMonthBy(secondDate)); }, [secondDate]); const changeSecondMonth = useCallback( (month: number) => { - secondGotoDate(dayjs(`${secondDate.year()}-${month < 10 ? "0" : ""}${month}-01`)); + secondGotoDate(dateUpdateMonth(secondDate, month - 1)); }, [secondDate, secondGotoDate] ); const changeSecondYear = useCallback( (year: number) => { - secondGotoDate(dayjs(`${year}-${secondDate.month() + 1}-01`)); + secondGotoDate(dateUpdateYear(secondDate, year)); }, [secondDate, secondGotoDate] ); @@ -186,19 +217,17 @@ const Datepicker: React.FC = ({ useEffect(() => { if (value && value.startDate && value.endDate) { - const startDate = dayjs(value.startDate); - const endDate = dayjs(value.endDate); - const validDate = startDate.isValid() && endDate.isValid(); - const condition = - validDate && (startDate.isSame(endDate) || startDate.isBefore(endDate)); - if (condition) { + if (dateIsSameOrBefore(value.startDate, value.endDate, "date")) { setPeriod({ - start: formatDate(startDate), - end: formatDate(endDate) + start: value.startDate, + end: value.endDate }); + setInputText( - `${formatDate(startDate, displayFormat)}${ - asSingle ? "" : ` ${separator} ${formatDate(endDate, displayFormat)}` + `${dateFormat(value.startDate, displayFormat, i18n)}${ + asSingle + ? "" + : ` ${separator} ${dateFormat(value.endDate, displayFormat, i18n)}` }` ); } @@ -209,34 +238,54 @@ const Datepicker: React.FC = ({ start: null, end: null }); + setInputText(""); } - }, [asSingle, value, displayFormat, separator]); + }, [asSingle, value, displayFormat, separator, i18n]); useEffect(() => { - if (startFrom && dayjs(startFrom).isValid()) { + if (startFrom && dateIsValid(startFrom)) { const startDate = value?.startDate; const endDate = value?.endDate; - if (startDate && dayjs(startDate).isValid()) { - setFirstDate(dayjs(startDate)); + + if (startDate && dateIsValid(startDate)) { + setFirstDate(startDate); if (!asSingle) { if ( endDate && - dayjs(endDate).isValid() && - dayjs(endDate).startOf("month").isAfter(dayjs(startDate)) + dateIsValid(endDate) && + dateIsAfter(firstDayOfMonth(endDate), startDate, "date") ) { - setSecondDate(dayjs(endDate)); + setSecondDate(endDate); } else { - setSecondDate(nextMonth(dayjs(startDate))); + setSecondDate(nextMonthBy(startDate)); } } } else { - setFirstDate(dayjs(startFrom)); - setSecondDate(nextMonth(dayjs(startFrom))); + setFirstDate(startFrom); + setSecondDate(nextMonthBy(startFrom)); } } }, [asSingle, startFrom, value]); + useEffect(() => { + const handleEscapeKey = (event: KeyboardEvent) => { + const container = calendarContainerRef.current; + + if (!container || !container.classList.contains("block") || event.key !== "Escape") { + return; + } + + hideDatepicker(); + }; + + document.addEventListener("keydown", handleEscapeKey); + + return () => { + document.removeEventListener("keydown", handleEscapeKey); + }; + }, [hideDatepicker]); + // Variables const safePrimaryColor = useMemo(() => { if (COLORS.includes(primaryColor)) { @@ -244,77 +293,99 @@ const Datepicker: React.FC = ({ } return DEFAULT_COLOR; }, [primaryColor]); + const contextValues = useMemo(() => { + if (minDate && !dateIsValid(minDate)) { + /* eslint-disable */ + console.error(`minDate (${minDate}) is invalid date`); + /* eslint-enable */ + } + + if (maxDate && !dateIsValid(maxDate)) { + /* eslint-disable */ + console.error(`minDate (${maxDate}) is invalid date`); + /* eslint-enable */ + } + + if (!i18n || i18n.length === 0) { + /* eslint-disable */ + console.error(`i18n (${i18n}) is invalid`); + /* eslint-enable */ + } + return { + arrowContainer: arrowRef, asSingle, - primaryColor: safePrimaryColor, - configs, calendarContainer: calendarContainerRef, - arrowContainer: arrowRef, - hideDatepicker, - period, - changePeriod: (newPeriod: Period) => setPeriod(newPeriod), - dayHover, - changeDayHover: (newDay: string | null) => setDayHover(newDay), - inputText, - changeInputText: (newText: string) => setInputText(newText), - updateFirstDate: (newDate: dayjs.Dayjs) => firstGotoDate(newDate), changeDatepickerValue: onChange, - showFooter, - placeholder, - separator, - i18n, - value, - disabled, - inputClassName, + changeDayHover: (newDay: DateType) => setDayHover(newDay), + changeInputText: (newText: string) => setInputText(newText), + changePeriod: (newPeriod: Period) => setPeriod(newPeriod), + classNames, + configs, containerClassName, - toggleClassName, - toggleIcon, - readOnly, - displayFormat, - minDate, - maxDate, dateLooking, + dayHover, + disabled, disabledDates, + displayFormat, + hideDatepicker, + i18n: i18n && i18n.length > 0 ? i18n : LANGUAGE, + input, + setInput: (value: HTMLInputElement | null) => setInput(value), + inputClassName, inputId, inputName, - startWeekOn, - classNames, + inputText, + maxDate, + minDate, onChange, - input: inputRef, - popoverDirection + period, + placeholder, + popoverDirection, + primaryColor: safePrimaryColor, + readOnly, + required, + separator, + showFooter, + startWeekOn: startWeekOn || START_WEEK, + toggleClassName, + toggleIcon, + updateFirstDate: (newDate: Date) => firstGotoDate(newDate), + value }; }, [ + minDate, + maxDate, + i18n, asSingle, - safePrimaryColor, - configs, - hideDatepicker, - period, - dayHover, - inputText, onChange, - showFooter, - placeholder, - separator, - i18n, - value, - disabled, - inputClassName, + classNames, + configs, containerClassName, - toggleClassName, - toggleIcon, - readOnly, - displayFormat, - minDate, - maxDate, dateLooking, + dayHover, + disabled, disabledDates, + displayFormat, + hideDatepicker, + input, + inputClassName, inputId, inputName, - startWeekOn, - classNames, - inputRef, + inputText, + period, + placeholder, popoverDirection, + safePrimaryColor, + readOnly, + required, + separator, + showFooter, + startWeekOn, + toggleClassName, + toggleIcon, + value, firstGotoDate ]); @@ -323,19 +394,26 @@ const Datepicker: React.FC = ({ return typeof containerClassName === "function" ? containerClassName(defaultContainerClassName) : typeof containerClassName === "string" && containerClassName !== "" - ? containerClassName - : defaultContainerClassName; + ? containerClassName + : defaultContainerClassName; }, [containerClassName]); + const popupClassNameOverload = useMemo(() => { + const defaultPopupClassName = + "transition-all ease-out duration-300 absolute z-10 mt-[1px] text-sm lg:text-xs 2xl:text-sm translate-y-4 opacity-0 hidden"; + return typeof popupClassName === "function" + ? popupClassName(defaultPopupClassName) + : typeof popupClassName === "string" && popupClassName !== "" + ? popupClassName + : defaultPopupClassName; + }, [popupClassName]); + return (
- + -
+
diff --git a/src/components/Footer.tsx b/src/components/Footer.tsx index 40f8fd2b..cfe6007f 100644 --- a/src/components/Footer.tsx +++ b/src/components/Footer.tsx @@ -1,14 +1,13 @@ -import dayjs from "dayjs"; -import React, { useCallback, useContext } from "react"; +import { useCallback, useContext } from "react"; -import { DATE_FORMAT } from "../constants"; import DatepickerContext from "../contexts/DatepickerContext"; -import { PrimaryButton, SecondaryButton } from "./utils"; +import PrimaryButton from "./PrimaryButton"; +import SecondaryButton from "./SecondaryButton"; -const Footer: React.FC = () => { +const Footer = () => { // Contexts - const { hideDatepicker, period, changeDatepickerValue, configs, classNames } = + const { hideDatepicker, period, changeDatepickerValue, configs, classNames, input } = useContext(DatepickerContext); // Functions @@ -30,13 +29,17 @@ const Footer: React.FC = () => { > <>{configs?.footer?.cancel ? configs.footer.cancel : "Cancel"} + { if (period.start && period.end) { - changeDatepickerValue({ - startDate: dayjs(period.start).format(DATE_FORMAT), - endDate: dayjs(period.end).format(DATE_FORMAT) - }); + changeDatepickerValue( + { + startDate: period.start, + endDate: period.end + }, + input + ); hideDatepicker(); } }} diff --git a/src/components/Input.tsx b/src/components/Input.tsx index 38fc856e..f34f1b0f 100644 --- a/src/components/Input.tsx +++ b/src/components/Input.tsx @@ -1,17 +1,13 @@ -import dayjs from "dayjs"; -import React, { useCallback, useContext, useEffect, useRef } from "react"; +import { ChangeEvent, KeyboardEvent, useCallback, useContext, useEffect, useRef } from "react"; -import { BORDER_COLOR, DATE_FORMAT, RING_COLOR } from "../constants"; +import { BORDER_COLOR, RING_COLOR } from "../constants"; import DatepickerContext from "../contexts/DatepickerContext"; -import { dateIsValid, parseFormattedDate } from "../helpers"; +import { dateAdd, dateIsBefore, dateStringToDate } from "../libs/date"; +import { DateType } from "../types"; import ToggleButton from "./ToggleButton"; -type Props = { - setContextRef?: (ref: React.RefObject) => void; -}; - -const Input: React.FC = (e: Props) => { +const Input = () => { // Context const { primaryColor, @@ -36,7 +32,10 @@ const Input: React.FC = (e: Props) => { inputId, inputName, classNames, - popoverDirection + popoverDirection, + required, + input, + setInput } = useContext(DatepickerContext); // UseRefs @@ -60,43 +59,40 @@ const Input: React.FC = (e: Props) => { return typeof inputClassName === "function" ? inputClassName(defaultInputClassName) : typeof inputClassName === "string" && inputClassName !== "" - ? inputClassName - : defaultInputClassName; + ? inputClassName + : defaultInputClassName; }, [inputRef, classNames, primaryColor, inputClassName]); const handleInputChange = useCallback( - (e: React.ChangeEvent) => { + (e: ChangeEvent) => { const inputValue = e.target.value; - const dates = []; + const dates: Date[] = []; if (asSingle) { - const date = parseFormattedDate(inputValue, displayFormat); - if (dateIsValid(date.toDate())) { - dates.push(date.format(DATE_FORMAT)); + const date = dateStringToDate(inputValue); + if (date) { + dates.push(date); } } else { const parsed = inputValue.split(separator); - let startDate = null; - let endDate = null; + let startDate: DateType; + let endDate: DateType; if (parsed.length === 2) { - startDate = parseFormattedDate(parsed[0], displayFormat); - endDate = parseFormattedDate(parsed[1], displayFormat); + dateStringToDate(parsed[0]); + startDate = dateStringToDate(parsed[0]); + endDate = dateStringToDate(parsed[1]); } else { const middle = Math.floor(inputValue.length / 2); - startDate = parseFormattedDate(inputValue.slice(0, middle), displayFormat); - endDate = parseFormattedDate(inputValue.slice(middle), displayFormat); + startDate = dateStringToDate(inputValue.slice(0, middle)); + endDate = dateStringToDate(inputValue.slice(middle)); } - if ( - dateIsValid(startDate.toDate()) && - dateIsValid(endDate.toDate()) && - startDate.isBefore(endDate) - ) { - dates.push(startDate.format(DATE_FORMAT)); - dates.push(endDate.format(DATE_FORMAT)); + if (startDate && endDate && dateIsBefore(startDate, endDate, "date")) { + dates.push(startDate); + dates.push(endDate); } } @@ -108,17 +104,21 @@ const Input: React.FC = (e: Props) => { }, e.target ); - if (dates[1]) changeDayHover(dayjs(dates[1]).add(-1, "day").format(DATE_FORMAT)); - else changeDayHover(dates[0]); + + if (dates[1]) { + changeDayHover(dateAdd(dates[1], -1, "day")); + } else { + changeDayHover(dates[0]); + } } changeInputText(e.target.value); }, - [asSingle, displayFormat, separator, changeDatepickerValue, changeDayHover, changeInputText] + [asSingle, separator, changeDatepickerValue, changeDayHover, changeInputText] ); const handleInputKeyDown = useCallback( - (e: React.KeyboardEvent) => { + (e: KeyboardEvent) => { if (e.key === "Enter") { const input = inputRef.current; if (input) { @@ -158,52 +158,51 @@ const Input: React.FC = (e: Props) => { return typeof toggleClassName === "function" ? toggleClassName(defaultToggleClassName) : typeof toggleClassName === "string" && toggleClassName !== "" - ? toggleClassName - : defaultToggleClassName; + ? toggleClassName + : defaultToggleClassName; }, [toggleClassName, buttonRef, classNames]); // UseEffects && UseLayoutEffect useEffect(() => { - if (inputRef && e.setContextRef && typeof e.setContextRef === "function") { - e.setContextRef(inputRef); + if (!input && inputRef?.current) { + setInput(inputRef.current); } - }, [e, inputRef]); + }, [input, inputRef, setInput]); useEffect(() => { const button = buttonRef?.current; + if (!button) return; + function focusInput(e: Event) { e.stopPropagation(); const input = inputRef.current; - if (input) { - input.focus(); - if (inputText) { - changeInputText(""); - if (dayHover) { - changeDayHover(null); - } - if (period.start && period.end) { - changeDatepickerValue( - { - startDate: null, - endDate: null - }, - input - ); - } - } + if (!input) return; + + input.focus(); + + if (!inputText) return; + + changeInputText(""); + if (dayHover) { + changeDayHover(null); + } + if (period.start && period.end) { + changeDatepickerValue( + { + startDate: null, + endDate: null + }, + input + ); } } - if (button) { - button.addEventListener("click", focusInput); - } + button.addEventListener("click", focusInput); return () => { - if (button) { - button.removeEventListener("click", focusInput); - } + button.removeEventListener("click", focusInput); }; }, [ changeDatepickerValue, @@ -273,6 +272,7 @@ const Input: React.FC = (e: Props) => { className={getClassName()} disabled={disabled} readOnly={readOnly} + required={required} placeholder={ placeholder ? placeholder diff --git a/src/components/PrimaryButton.tsx b/src/components/PrimaryButton.tsx new file mode 100644 index 00000000..e596a94e --- /dev/null +++ b/src/components/PrimaryButton.tsx @@ -0,0 +1,31 @@ +import { useCallback, useContext } from "react"; + +import { BG_COLOR, BORDER_COLOR, RING_COLOR } from "../constants"; +import DatepickerContext from "../contexts/DatepickerContext"; +import { ButtonProps } from "../types"; + +const PrimaryButton = (props: ButtonProps) => { + const { children, onClick, disabled = false } = props; + + // Contexts + const { primaryColor } = useContext(DatepickerContext); + const bgColor = BG_COLOR["500"][primaryColor as keyof (typeof BG_COLOR)["500"]]; + const borderColor = BORDER_COLOR["500"][primaryColor as keyof (typeof BORDER_COLOR)["500"]]; + const bgColorHover = BG_COLOR.hover[primaryColor as keyof typeof BG_COLOR.hover]; + const ringColor = RING_COLOR.focus[primaryColor as keyof typeof RING_COLOR.focus]; + + // Functions + const getClassName = useCallback(() => { + return `w-full transition-all duration-300 ${bgColor} ${borderColor} text-white font-medium border px-4 py-2 text-sm rounded-md focus:ring-2 focus:ring-offset-2 ${bgColorHover} ${ringColor} ${ + disabled ? " cursor-no-drop" : "" + }`; + }, [bgColor, bgColorHover, borderColor, disabled, ringColor]); + + return ( + + ); +}; + +export default PrimaryButton; diff --git a/src/components/RoundedButton.tsx b/src/components/RoundedButton.tsx new file mode 100644 index 00000000..6f724f15 --- /dev/null +++ b/src/components/RoundedButton.tsx @@ -0,0 +1,41 @@ +import { useCallback, useContext } from "react"; + +import { BUTTON_COLOR } from "../constants"; +import DatepickerContext from "../contexts/DatepickerContext"; +import { ButtonProps } from "../types"; + +const RoundedButton = (props: ButtonProps) => { + const { + children, + onClick, + disabled, + roundedFull = false, + padding = "py-[0.55rem]", + active = false + } = props; + + // Contexts + const { primaryColor } = useContext(DatepickerContext); + + // Functions + const getClassName = useCallback(() => { + const darkClass = "dark:text-white/70 dark:hover:bg-white/10 dark:focus:bg-white/10"; + const activeClass = active ? "font-semibold bg-gray-50 dark:bg-white/5" : ""; + const defaultClass = !roundedFull + ? `w-full tracking-wide ${darkClass} ${activeClass} transition-all duration-300 px-3 ${padding} uppercase hover:bg-gray-100 rounded-md focus:ring-1` + : `${darkClass} ${activeClass} transition-all duration-300 hover:bg-gray-100 rounded-full p-[0.45rem] focus:ring-1`; + const buttonFocusColor = + BUTTON_COLOR.focus[primaryColor as keyof typeof BUTTON_COLOR.focus]; + const disabledClass = disabled ? "line-through" : ""; + + return `${defaultClass} ${buttonFocusColor} ${disabledClass}`; + }, [disabled, padding, primaryColor, roundedFull, active]); + + return ( + + ); +}; + +export default RoundedButton; diff --git a/src/components/SecondaryButton.tsx b/src/components/SecondaryButton.tsx new file mode 100644 index 00000000..caba956d --- /dev/null +++ b/src/components/SecondaryButton.tsx @@ -0,0 +1,26 @@ +import { useCallback, useContext } from "react"; + +import { RING_COLOR } from "../constants"; +import DatepickerContext from "../contexts/DatepickerContext"; +import { ButtonProps } from "../types"; + +const SecondaryButton = (props: ButtonProps) => { + const { children, onClick, disabled = false } = props; + + // Contexts + const { primaryColor } = useContext(DatepickerContext); + + // Functions + const getClassName: () => string = useCallback(() => { + const ringColor = RING_COLOR.focus[primaryColor as keyof typeof RING_COLOR.focus]; + return `w-full transition-all duration-300 bg-white dark:text-gray-700 font-medium border border-gray-300 px-4 py-2 text-sm rounded-md focus:ring-2 focus:ring-offset-2 hover:bg-gray-50 ${ringColor}`; + }, [primaryColor]); + + return ( + + ); +}; + +export default SecondaryButton; diff --git a/src/components/Shortcuts.tsx b/src/components/Shortcuts.tsx index ae54b21c..5fbeea23 100644 --- a/src/components/Shortcuts.tsx +++ b/src/components/Shortcuts.tsx @@ -1,19 +1,18 @@ -import dayjs from "dayjs"; -import React, { useCallback, useContext, useMemo } from "react"; +import { memo, ReactNode, useCallback, useContext, useMemo } from "react"; -import { DATE_FORMAT, TEXT_COLOR } from "../constants"; +import { TEXT_COLOR } from "../constants"; import DEFAULT_SHORTCUTS from "../constants/shortcuts"; import DatepickerContext from "../contexts/DatepickerContext"; +import { dateIsSameOrBefore } from "../libs/date"; import { Period, ShortcutsItem } from "../types"; interface ItemTemplateProps { - children: JSX.Element; + children: ReactNode; key: number; item: ShortcutsItem | ShortcutsItem[]; } -// eslint-disable-next-line react/display-name -const ItemTemplate = React.memo((props: ItemTemplateProps) => { +const ItemTemplate = memo((props: ItemTemplateProps) => { const { primaryColor, period, @@ -22,11 +21,12 @@ const ItemTemplate = React.memo((props: ItemTemplateProps) => { dayHover, changeDayHover, hideDatepicker, - changeDatepickerValue + changeDatepickerValue, + input } = useContext(DatepickerContext); // Functions - const getClassName: () => string = useCallback(() => { + const getClassName = useCallback(() => { const textColor = TEXT_COLOR["600"][primaryColor as keyof (typeof TEXT_COLOR)["600"]]; const textColorHover = TEXT_COLOR.hover[primaryColor as keyof typeof TEXT_COLOR.hover]; return `whitespace-nowrap w-1/2 md:w-1/3 lg:w-auto transition-all duration-300 hover:bg-gray-100 dark:hover:bg-white/10 p-2 rounded cursor-pointer ${textColor} ${textColorHover}`; @@ -44,11 +44,15 @@ const ItemTemplate = React.memo((props: ItemTemplateProps) => { }); } changePeriod(item); - changeDatepickerValue({ - startDate: item.start, - endDate: item.end - }); - updateFirstDate(dayjs(item.start)); + changeDatepickerValue( + { + startDate: item.start, + endDate: item.end + }, + input + ); + + if (item.start) updateFirstDate(item.start); hideDatepicker(); }, [ @@ -57,6 +61,7 @@ const ItemTemplate = React.memo((props: ItemTemplateProps) => { changePeriod, dayHover, hideDatepicker, + input, period.end, period.start, updateFirstDate @@ -79,7 +84,9 @@ const ItemTemplate = React.memo((props: ItemTemplateProps) => { ); }); -const Shortcuts: React.FC = () => { +ItemTemplate.displayName = "ItemTemplate"; + +const Shortcuts = () => { // Contexts const { configs } = useContext(DatepickerContext); @@ -97,26 +104,23 @@ const Shortcuts: React.FC = () => { return [[key, DEFAULT_SHORTCUTS[key]]]; } - const { text, period } = customConfig as { - text: string; - period: { start: string; end: string }; - }; + const { text, period } = customConfig as ShortcutsItem; + if (!text || !period) { return []; } - const start = dayjs(period.start); - const end = dayjs(period.end); + const { start, end } = period; - if (start.isValid() && end.isValid() && (start.isBefore(end) || start.isSame(end))) { + if (dateIsSameOrBefore(start, end, "date")) { return [ [ text, { text, period: { - start: start.format(DATE_FORMAT), - end: end.format(DATE_FORMAT) + start: start, + end: end } } ] @@ -134,7 +138,7 @@ const Shortcuts: React.FC = () => { return shortcutOptions?.length ? (
    - {shortcutOptions.map(([key, item], index: number) => + {shortcutOptions.map(([key, item], index) => Array.isArray(item) ? ( item.map((item, index) => ( diff --git a/src/components/ToggleButton.tsx b/src/components/ToggleButton.tsx index ebe5d651..4a397e8d 100644 --- a/src/components/ToggleButton.tsx +++ b/src/components/ToggleButton.tsx @@ -1,12 +1,11 @@ -import React from "react"; +import CloseIcon from "./icons/CloseIcon"; +import DateIcon from "./icons/DateIcon"; -import { CloseIcon, DateIcon } from "./utils"; - -interface ToggleButtonProps { +interface Props { isEmpty: boolean; } -const ToggleButton: React.FC = (e: ToggleButtonProps): JSX.Element => { +const ToggleButton = (e: Props) => { return e.isEmpty ? : ; }; diff --git a/src/components/VerticalDash.tsx b/src/components/VerticalDash.tsx new file mode 100644 index 00000000..c475a181 --- /dev/null +++ b/src/components/VerticalDash.tsx @@ -0,0 +1,14 @@ +import { useContext } from "react"; + +import { BG_COLOR } from "../constants"; +import DatepickerContext from "../contexts/DatepickerContext"; + +const VerticalDash = () => { + // Contexts + const { primaryColor } = useContext(DatepickerContext); + const bgColor = BG_COLOR["500"][primaryColor as keyof (typeof BG_COLOR)["500"]]; + + return