From 4f2944a9746f9be96149239d187f6c5f31f165db Mon Sep 17 00:00:00 2001 From: Wahyu Kurniawan Date: Tue, 20 Feb 2024 16:31:56 +0700 Subject: [PATCH 1/8] =?UTF-8?q?=F0=9F=94=A7=20chore:=20install=20`date-fns?= =?UTF-8?q?`=20and=20`react-calendar`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/frontend/package.json | 3 +- yarn.lock | 62 ++++++++++++++++++++++++++++++---- 2 files changed, 57 insertions(+), 8 deletions(-) diff --git a/packages/frontend/package.json b/packages/frontend/package.json index a2a77ba..4973668 100644 --- a/packages/frontend/package.json +++ b/packages/frontend/package.json @@ -13,13 +13,14 @@ "@types/react": "^18.2.42", "@types/react-dom": "^18.2.17", "assert": "^2.1.0", - "date-fns": "^3.0.1", + "date-fns": "^3.3.1", "downshift": "^8.2.3", "eslint-config-react-app": "^7.0.1", "gql-client": "^1.0.0", "luxon": "^3.4.4", "octokit": "^3.1.2", "react": "^18.2.0", + "react-calendar": "^4.8.0", "react-code-blocks": "^0.1.6", "react-day-picker": "^8.9.1", "react-dom": "^18.2.0", diff --git a/yarn.lock b/yarn.lock index e471349..8845c97 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3910,6 +3910,18 @@ dependencies: "@types/node" "*" +"@types/lodash.memoize@^4.1.7": + version "4.1.9" + resolved "https://registry.yarnpkg.com/@types/lodash.memoize/-/lodash.memoize-4.1.9.tgz#9f8912d39b6e450c0d342a2b74c99d331bf2016b" + integrity sha512-glY1nQuoqX4Ft8Uk+KfJudOD7DQbbEDF6k9XpGncaohW3RW4eSWBlx6AA0fZCrh40tZcQNH4jS/Oc59J6Eq+aw== + dependencies: + "@types/lodash" "*" + +"@types/lodash@*": + version "4.14.202" + resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.202.tgz#f09dbd2fb082d507178b2f2a5c7e74bd72ff98f8" + integrity sha512-OvlIYQK9tNneDlS0VN54LLd5uiPCBOp7gS5Z0f1mjoJYBrtStzgmJBxONW3U6OZqdtNzZPmn9BS/7WI7BFFcFQ== + "@types/long@^4.0.0", "@types/long@^4.0.1": version "4.0.2" resolved "https://registry.yarnpkg.com/@types/long/-/long-4.0.2.tgz#b74129719fc8d11c01868010082d483b7545591a" @@ -4687,6 +4699,11 @@ "@webassemblyjs/ast" "1.11.6" "@xtuc/long" "4.2.2" +"@wojtekmaj/date-utils@^1.1.3": + version "1.5.1" + resolved "https://registry.yarnpkg.com/@wojtekmaj/date-utils/-/date-utils-1.5.1.tgz#c3cd67177ac781cfa5736219d702a55a2aea5f2b" + integrity sha512-+i7+JmNiE/3c9FKxzWFi2IjRJ+KzZl1QPu6QNrsgaa2MuBgXvUy4gA1TVzf/JMdIIloB76xSKikTWuyYAIVLww== + "@wry/caches@^1.0.0": version "1.0.1" resolved "https://registry.yarnpkg.com/@wry/caches/-/caches-1.0.1.tgz#8641fd3b6e09230b86ce8b93558d44cf1ece7e52" @@ -6144,6 +6161,11 @@ clone@^1.0.2: resolved "https://registry.yarnpkg.com/clone/-/clone-1.0.4.tgz#da309cc263df15994c688ca902179ca3c7cd7c7e" integrity sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg== +clsx@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/clsx/-/clsx-2.1.0.tgz#e851283bcb5c80ee7608db18487433f7b23f77cb" + integrity sha512-m3iNNWpd9rl3jvvcBnu70ylMdrXt8Vlq4HYadnU5fwcOtvkSQWPmj7amUcDT2qYI7risszBjI5AUIUox9D16pg== + cmd-shim@6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/cmd-shim/-/cmd-shim-6.0.1.tgz#a65878080548e1dca760b3aea1e21ed05194da9d" @@ -6821,10 +6843,10 @@ data-urls@^2.0.0: whatwg-mimetype "^2.3.0" whatwg-url "^8.0.0" -date-fns@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-3.0.1.tgz#a95b3e8296e72cf57c99819f37679aa27ca65ec4" - integrity sha512-cr9igCUa0QSqgAMj7JOrYTY6Nh1rmyGrFDko7ADqfmaQqP/I2N4rlfrLl7AWuzDaoIpz6MNjoEcTPzgZYIrhnA== +date-fns@^3.3.1: + version "3.3.1" + resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-3.3.1.tgz#7581daca0892d139736697717a168afbb908cfed" + integrity sha512-y8e109LYGgoQDveiEBD3DYXKba1jWf5BA8YU1FL5Tvm0BTdEfy54WLCwnuYWZNnzzvALy/QQ4Hov+Q9RVRv+Zw== dateformat@^3.0.3: version "3.0.3" @@ -8720,6 +8742,14 @@ get-tsconfig@^4.7.0: dependencies: resolve-pkg-maps "^1.0.0" +get-user-locale@^2.2.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/get-user-locale/-/get-user-locale-2.3.1.tgz#fc7319429c8a70fac01b3b2a0b08b0c71c1d3fe2" + integrity sha512-VEvcsqKYx7zhZYC1CjecrDC5ziPSpl1gSm0qFFJhHSGDrSC+x4+p1KojWC/83QX//j476gFhkVXP/kNUc9q+bQ== + dependencies: + "@types/lodash.memoize" "^4.1.7" + lodash.memoize "^4.1.1" + git-raw-commits@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/git-raw-commits/-/git-raw-commits-3.0.0.tgz#5432f053a9744f67e8db03dbc48add81252cfdeb" @@ -11012,7 +11042,7 @@ lodash.isstring@^4.0.1: resolved "https://registry.yarnpkg.com/lodash.isstring/-/lodash.isstring-4.0.1.tgz#d527dfb5456eca7cc9bb95d5daeaf88ba54a5451" integrity sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw== -lodash.memoize@^4.1.2: +lodash.memoize@^4.1.1, lodash.memoize@^4.1.2: version "4.1.2" resolved "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz" integrity sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag== @@ -11065,7 +11095,7 @@ long@^5.2.0: resolved "https://registry.yarnpkg.com/long/-/long-5.2.3.tgz#a3ba97f3877cf1d778eccbcb048525ebb77499e1" integrity sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q== -loose-envify@^1.1.0, loose-envify@^1.4.0: +loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.4.0: version "1.4.0" resolved "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz" integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q== @@ -13281,7 +13311,7 @@ promzard@^1.0.0: dependencies: read "^2.0.0" -prop-types@15.8.1, prop-types@^15.7.2, prop-types@^15.8.1: +prop-types@15.8.1, prop-types@^15.6.0, prop-types@^15.7.2, prop-types@^15.8.1: version "15.8.1" resolved "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz" integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg== @@ -13440,6 +13470,17 @@ react-app-polyfill@^3.0.0: regenerator-runtime "^0.13.9" whatwg-fetch "^3.6.2" +react-calendar@^4.8.0: + version "4.8.0" + resolved "https://registry.yarnpkg.com/react-calendar/-/react-calendar-4.8.0.tgz#61edbba6d17e7ef8a8012de9143b5e5ff41104c8" + integrity sha512-qFgwo+p58sgv1QYMI1oGNaop90eJVKuHTZ3ZgBfrrpUb+9cAexxsKat0sAszgsizPMVo7vOXedV7Lqa0GQGMvA== + dependencies: + "@wojtekmaj/date-utils" "^1.1.3" + clsx "^2.0.0" + get-user-locale "^2.2.1" + prop-types "^15.6.0" + warning "^4.0.0" + react-code-blocks@^0.1.6: version "0.1.6" resolved "https://registry.yarnpkg.com/react-code-blocks/-/react-code-blocks-0.1.6.tgz#ec64e7899223d3e910eb916465a66d95ce1ae1b2" @@ -15880,6 +15921,13 @@ walker@^1.0.7: dependencies: makeerror "1.0.12" +warning@^4.0.0: + version "4.0.3" + resolved "https://registry.yarnpkg.com/warning/-/warning-4.0.3.tgz#16e9e077eb8a86d6af7d64aa1e05fd85b4678ca3" + integrity sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w== + dependencies: + loose-envify "^1.0.0" + watchpack@^2.4.0: version "2.4.0" resolved "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz" From 0a5a1b8c5550c8bca0acaa8a47673e19c60f54e5 Mon Sep 17 00:00:00 2001 From: Wahyu Kurniawan Date: Tue, 20 Feb 2024 16:32:35 +0700 Subject: [PATCH 2/8] =?UTF-8?q?=F0=9F=94=A7=20chore:=20add=20vscode=20sett?= =?UTF-8?q?ings=20to=20enable=20tailwind=20variant=20intellisense?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .vscode/settings.json | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .vscode/settings.json diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..f3e0823 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,6 @@ +{ + // IntelliSense for taiwind variants + "tailwindCSS.experimental.classRegex": [ + ["tv\\((([^()]*|\\([^()]*\\))*)\\)", "[\"'`]([^\"'`]*).*?[\"'`]"] + ] +} From cc97ddff9d828592d38827a981173ffcec6ce83b Mon Sep 17 00:00:00 2001 From: Wahyu Kurniawan Date: Tue, 20 Feb 2024 16:33:12 +0700 Subject: [PATCH 3/8] =?UTF-8?q?=E2=9A=A1=EF=B8=8F=20feat:=20create=20chevr?= =?UTF-8?q?on=20left,=20right,=20and=20grabbed=20horizontal=20component?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../CustomIcon/ChevronGrabberHorizontal.tsx | 20 ++++++++++++++++++ .../shared/CustomIcon/ChevronLeft.tsx | 21 +++++++++++++++++++ .../shared/CustomIcon/ChevronRight.tsx | 21 +++++++++++++++++++ .../src/components/shared/CustomIcon/index.ts | 3 +++ 4 files changed, 65 insertions(+) create mode 100644 packages/frontend/src/components/shared/CustomIcon/ChevronGrabberHorizontal.tsx create mode 100644 packages/frontend/src/components/shared/CustomIcon/ChevronLeft.tsx create mode 100644 packages/frontend/src/components/shared/CustomIcon/ChevronRight.tsx diff --git a/packages/frontend/src/components/shared/CustomIcon/ChevronGrabberHorizontal.tsx b/packages/frontend/src/components/shared/CustomIcon/ChevronGrabberHorizontal.tsx new file mode 100644 index 0000000..b62f79d --- /dev/null +++ b/packages/frontend/src/components/shared/CustomIcon/ChevronGrabberHorizontal.tsx @@ -0,0 +1,20 @@ +import React from 'react'; +import { CustomIcon, CustomIconProps } from './CustomIcon'; + +export const ChevronGrabberHorizontal = (props: CustomIconProps) => { + return ( + + + + ); +}; diff --git a/packages/frontend/src/components/shared/CustomIcon/ChevronLeft.tsx b/packages/frontend/src/components/shared/CustomIcon/ChevronLeft.tsx new file mode 100644 index 0000000..f9c4885 --- /dev/null +++ b/packages/frontend/src/components/shared/CustomIcon/ChevronLeft.tsx @@ -0,0 +1,21 @@ +import React from 'react'; +import { CustomIcon, CustomIconProps } from './CustomIcon'; + +export const ChevronLeft = (props: CustomIconProps) => { + return ( + + + + ); +}; diff --git a/packages/frontend/src/components/shared/CustomIcon/ChevronRight.tsx b/packages/frontend/src/components/shared/CustomIcon/ChevronRight.tsx new file mode 100644 index 0000000..75da005 --- /dev/null +++ b/packages/frontend/src/components/shared/CustomIcon/ChevronRight.tsx @@ -0,0 +1,21 @@ +import React from 'react'; +import { CustomIcon, CustomIconProps } from './CustomIcon'; + +export const ChevronRight = (props: CustomIconProps) => { + return ( + + + + ); +}; diff --git a/packages/frontend/src/components/shared/CustomIcon/index.ts b/packages/frontend/src/components/shared/CustomIcon/index.ts index d50d942..e4b5103 100644 --- a/packages/frontend/src/components/shared/CustomIcon/index.ts +++ b/packages/frontend/src/components/shared/CustomIcon/index.ts @@ -1,2 +1,5 @@ export * from './PlusIcon'; export * from './CustomIcon'; +export * from './ChevronGrabberHorizontal'; +export * from './ChevronLeft'; +export * from './ChevronRight'; From ad7dd1920a3a0f74d3f2520a6a284b49da7a7b97 Mon Sep 17 00:00:00 2001 From: Wahyu Kurniawan Date: Tue, 20 Feb 2024 16:33:26 +0700 Subject: [PATCH 4/8] =?UTF-8?q?=E2=9A=A1=EF=B8=8F=20feat:=20create=20calen?= =?UTF-8?q?dar=20component?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/shared/Calendar/Calendar.css | 128 ++++++++ .../shared/Calendar/Calendar.theme.ts | 49 +++ .../components/shared/Calendar/Calendar.tsx | 292 ++++++++++++++++++ .../src/components/shared/Calendar/index.ts | 1 + 4 files changed, 470 insertions(+) create mode 100644 packages/frontend/src/components/shared/Calendar/Calendar.css create mode 100644 packages/frontend/src/components/shared/Calendar/Calendar.theme.ts create mode 100644 packages/frontend/src/components/shared/Calendar/Calendar.tsx create mode 100644 packages/frontend/src/components/shared/Calendar/index.ts diff --git a/packages/frontend/src/components/shared/Calendar/Calendar.css b/packages/frontend/src/components/shared/Calendar/Calendar.css new file mode 100644 index 0000000..85ce418 --- /dev/null +++ b/packages/frontend/src/components/shared/Calendar/Calendar.css @@ -0,0 +1,128 @@ +/* React Calendar */ +.react-calendar { + @apply border-none font-sans; +} + +/* Weekdays -- START */ +.react-calendar__month-view__weekdays { + @apply p-0 flex items-center justify-center; +} + +.react-calendar__month-view__weekdays__weekday { + @apply h-8 w-12 flex items-center justify-center p-0 font-medium text-xs text-elements-disabled mb-2; +} + +abbr[title] { + text-decoration: none; +} +/* Weekdays -- END */ + +/* Days -- START */ +.react-calendar__month-view__days { + @apply p-0 gap-0; +} + +.react-calendar__month-view__days__day--neighboringMonth { + @apply !text-elements-disabled; +} + +.react-calendar__month-view__days__day--neighboringMonth:hover { + @apply !text-elements-disabled !bg-transparent; +} + +.react-calendar__month-view__days__day--neighboringMonth { + @apply !text-elements-disabled !bg-transparent; +} + +/* For weekend days */ +.react-calendar__month-view__days__day--weekend { + /* color: ${colors.grey[950]} !important; */ +} + +.react-calendar__tile { + @apply h-12 w-12 text-elements-high-em; +} + +.react-calendar__tile:hover { + @apply bg-base-bg-emphasized rounded-lg; +} + +.react-calendar__tile:focus-visible { + @apply bg-base-bg-emphasized rounded-lg focus-ring z-10; +} + +.react-calendar__tile--now { + @apply bg-base-bg-emphasized text-elements-high-em rounded-lg; +} + +.react-calendar__tile--now:hover { + @apply bg-base-bg-emphasized text-elements-high-em rounded-lg; +} + +.react-calendar__tile--now:focus-visible { + @apply bg-base-bg-emphasized text-elements-high-em rounded-lg focus-ring; +} + +.react-calendar__tile--active { + @apply bg-controls-primary text-elements-on-primary rounded-lg; +} + +.react-calendar__tile--active:hover { + @apply bg-controls-primary-hovered; +} + +.react-calendar__tile--active:focus-visible { + @apply bg-controls-primary-hovered focus-ring; +} + +/* Range -- START */ +.react-calendar__tile--range { + @apply bg-controls-secondary text-elements-on-secondary rounded-none; +} + +.react-calendar__tile--range:hover { + @apply bg-controls-secondary-hovered text-elements-on-secondary rounded-none; +} + +.react-calendar__tile--range:focus-visible { + @apply bg-controls-secondary-hovered text-elements-on-secondary rounded-lg; +} + +.react-calendar__tile--rangeStart { + @apply bg-controls-primary text-elements-on-primary rounded-lg; +} + +.react-calendar__tile--rangeStart:hover { + @apply bg-controls-primary-hovered text-elements-on-primary rounded-lg; +} + +.react-calendar__tile--rangeStart:focus-visible { + @apply bg-controls-primary-hovered text-elements-on-primary rounded-lg focus-ring; +} + +.react-calendar__tile--rangeEnd { + @apply bg-controls-primary text-elements-on-primary rounded-lg; +} + +.react-calendar__tile--rangeEnd:hover { + @apply bg-controls-primary-hovered text-elements-on-primary rounded-lg; +} + +.react-calendar__tile--rangeEnd:focus-visible { + @apply bg-controls-primary-hovered text-elements-on-primary rounded-lg focus-ring; +} +/* Range -- END */ +/* Days -- END */ + +/* Months -- START */ +.react-calendar__tile--hasActive { + @apply bg-controls-primary text-elements-on-primary rounded-lg; +} + +.react-calendar__tile--hasActive:hover { + @apply bg-controls-primary-hovered text-elements-on-primary rounded-lg; +} + +.react-calendar__tile--hasActive:focus-visible { + @apply bg-controls-primary-hovered text-elements-on-primary rounded-lg focus-ring; +} diff --git a/packages/frontend/src/components/shared/Calendar/Calendar.theme.ts b/packages/frontend/src/components/shared/Calendar/Calendar.theme.ts new file mode 100644 index 0000000..e39e592 --- /dev/null +++ b/packages/frontend/src/components/shared/Calendar/Calendar.theme.ts @@ -0,0 +1,49 @@ +import { VariantProps, tv } from 'tailwind-variants'; + +export const calendarTheme = tv({ + slots: { + wrapper: [ + 'max-w-[352px]', + 'bg-surface-floating', + 'shadow-calendar', + 'rounded-xl', + ], + calendar: ['flex', 'flex-col', 'py-2', 'px-2', 'gap-2'], + navigation: [ + 'flex', + 'items-center', + 'justify-between', + 'gap-3', + 'py-2.5', + 'px-1', + ], + dropdowns: ['flex', 'items-center', 'justify-center', 'gap-1.5', 'flex-1'], + dropdown: [ + 'flex', + 'items-center', + 'gap-2', + 'px-2', + 'py-2', + 'rounded-lg', + 'border', + 'border-border-interactive', + 'text-elements-high-em', + 'shadow-field', + 'bg-white', + 'hover:bg-base-bg-alternate', + 'focus-visible:bg-base-bg-alternate', + ], + footer: [ + 'flex', + 'items-center', + 'justify-end', + 'py-3', + 'px-2', + 'gap-3', + 'border-t', + 'border-border-separator', + ], + }, +}); + +export type CalendarTheme = VariantProps; diff --git a/packages/frontend/src/components/shared/Calendar/Calendar.tsx b/packages/frontend/src/components/shared/Calendar/Calendar.tsx new file mode 100644 index 0000000..737eee7 --- /dev/null +++ b/packages/frontend/src/components/shared/Calendar/Calendar.tsx @@ -0,0 +1,292 @@ +import React, { + ComponentPropsWithRef, + MouseEvent, + ReactNode, + useCallback, + useState, +} from 'react'; +import { + Calendar as ReactCalendar, + CalendarProps as ReactCalendarProps, +} from 'react-calendar'; +import { Value } from 'react-calendar/dist/cjs/shared/types'; +import { CalendarTheme, calendarTheme } from './Calendar.theme'; +import { Button } from 'components/shared/Button'; +import { + ChevronGrabberHorizontal, + ChevronLeft, + ChevronRight, +} from 'components/shared/CustomIcon'; + +import './Calendar.css'; +import { format } from 'date-fns'; + +const CALENDAR_VIEW = ['month', 'year', 'decade', 'century'] as const; +export type CalendarView = (typeof CALENDAR_VIEW)[number]; + +/** + * Defines a custom set of props for a React calendar component by excluding specific props + * from the original ReactCalendarProps type. + * @type {CustomReactCalendarProps} + */ +type CustomReactCalendarProps = Omit< + ReactCalendarProps, + 'view' | 'showNavigation' | 'onClickMonth' | 'onClickYear' +>; + +export interface CalendarProps extends CustomReactCalendarProps, CalendarTheme { + /** + * Optional props for wrapping a component with a div element. + */ + wrapperProps?: ComponentPropsWithRef<'div'>; + /** + * Props for the calendar wrapper component. + */ + calendarWrapperProps?: ComponentPropsWithRef<'div'>; + /** + * Optional props for the footer component. + */ + footerProps?: ComponentPropsWithRef<'div'>; + /** + * Optional custom actions to be rendered. + */ + actions?: ReactNode; + /** + * Optional callback function that is called when a value is selected. + * @param {Value} value - The selected value + * @returns None + */ + onSelect?: (value: Value) => void; + /** + * Optional callback function that is called when a cancel action is triggered. + * @returns None + */ + onCancel?: () => void; +} + +/** + * Calendar component that allows users to select dates and navigate through months and years. + * @param {Object} CalendarProps - Props for the Calendar component. + * @returns {JSX.Element} A calendar component with navigation, date selection, and actions. + */ +export const Calendar = ({ + selectRange, + activeStartDate: activeStartDateProp, + value: valueProp, + wrapperProps, + calendarWrapperProps, + footerProps, + actions, + onSelect, + onCancel, + onChange: onChangeProp, + ...props +}: CalendarProps): JSX.Element => { + const { wrapper, calendar, navigation, dropdowns, dropdown, footer } = + calendarTheme(); + + const today = new Date(); + const currentMonth = format(today, 'MMM'); + const currentyear = format(today, 'yyyy'); + + const [view, setView] = useState('month'); + const [activeDate, setActiveDate] = useState( + activeStartDateProp ?? today, + ); + const [value, setValue] = useState(valueProp as Value); + const [month, setMonth] = useState(currentMonth); + const [year, setYear] = useState(currentyear); + + /** + * Update the navigation label based on the active date + */ + const changeNavigationLabel = useCallback( + (date: Date) => { + setMonth(format(date, 'MMM')); + setYear(format(date, 'yyyy')); + }, + [setMonth, setYear], + ); + + /** + * Change the active date base on the action and range + */ + const handleNavigate = useCallback( + (action: 'previous' | 'next', range: 'month' | 'year' | 'decade') => { + setActiveDate((date) => { + const newDate = new Date(date); + switch (range) { + case 'month': + newDate.setMonth( + action === 'previous' ? date.getMonth() - 1 : date.getMonth() + 1, + ); + break; + case 'year': + newDate.setFullYear( + action === 'previous' + ? date.getFullYear() - 1 + : date.getFullYear() + 1, + ); + break; + case 'decade': + newDate.setFullYear( + action === 'previous' + ? date.getFullYear() - 10 + : date.getFullYear() + 10, + ); + break; + } + changeNavigationLabel(newDate); + return newDate; + }); + }, + [setActiveDate, changeNavigationLabel], + ); + + /** + * Change the view of the calendar + */ + const handleChangeView = useCallback( + (view: CalendarView) => { + setView(view); + }, + [setView], + ); + + /** + * Change the active date and set the view to the selected type + * and also update the navigation label + */ + const handleChangeNavigation = useCallback( + (type: 'month' | 'year', date: Date) => { + setActiveDate(date); + changeNavigationLabel(date); + setView(type); + }, + [setActiveDate, changeNavigationLabel, setView], + ); + + const handlePrevious = useCallback(() => { + switch (view) { + case 'month': + return handleNavigate('previous', 'month'); + case 'year': + return handleNavigate('previous', 'year'); + case 'decade': + return handleNavigate('previous', 'decade'); + } + }, [view]); + + const handleNext = useCallback(() => { + switch (view) { + case 'month': + return handleNavigate('next', 'month'); + case 'year': + return handleNavigate('next', 'year'); + case 'decade': + return handleNavigate('next', 'decade'); + } + }, [view]); + + const handleChange = useCallback( + (newValue: Value, event: MouseEvent) => { + setValue(newValue); + + // Call the onChange prop if it exists + onChangeProp?.(newValue, event); + + /** + * Update the active date and navigation label + * + * NOTE: + * For range selection, the active date is not updated + * The user only can select multiple dates within the same month + */ + if (!selectRange) { + setActiveDate(newValue as Date); + changeNavigationLabel(newValue as Date); + } + }, + [setValue, setActiveDate, changeNavigationLabel, selectRange], + ); + + return ( +
+ {/* Calendar wrapper */} +
+ {/* Navigation */} +
+ +
+ + +
+ +
+ + {/* Calendar */} + handleChangeNavigation('month', date)} + onClickYear={(date) => handleChangeNavigation('year', date)} + /> +
+ + {/* Footer or CTA */} +
+ {actions ? ( + actions + ) : ( + <> + + + + )} +
+
+ ); +}; diff --git a/packages/frontend/src/components/shared/Calendar/index.ts b/packages/frontend/src/components/shared/Calendar/index.ts new file mode 100644 index 0000000..a723380 --- /dev/null +++ b/packages/frontend/src/components/shared/Calendar/index.ts @@ -0,0 +1 @@ +export * from './Calendar'; From 4f7f9cf9143ddd60a1ab4fd91852d50216153924 Mon Sep 17 00:00:00 2001 From: Wahyu Kurniawan Date: Tue, 20 Feb 2024 16:33:59 +0700 Subject: [PATCH 5/8] =?UTF-8?q?=F0=9F=93=9D=20docs:=20add=20calendar=20com?= =?UTF-8?q?ponent=20to=20the=20example=20page?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/frontend/src/pages/components/index.tsx | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/packages/frontend/src/pages/components/index.tsx b/packages/frontend/src/pages/components/index.tsx index 8c23f34..4ff2e58 100644 --- a/packages/frontend/src/pages/components/index.tsx +++ b/packages/frontend/src/pages/components/index.tsx @@ -1,4 +1,5 @@ import { Button, ButtonOrLinkProps } from 'components/shared/Button'; +import { Calendar } from 'components/shared/Calendar'; import { PlusIcon } from 'components/shared/CustomIcon'; import React from 'react'; @@ -13,6 +14,7 @@ const Page = () => { packages/frontend/src/pages/components/index.tsx

+
{/* Insert Components here */} @@ -69,6 +71,15 @@ const Page = () => { ))}
+ +
+
+

Calendar

+
+ + +
+
); From af0d28e6546d0b4f41d4f8a6a178fec04917d015 Mon Sep 17 00:00:00 2001 From: Wahyu Kurniawan Date: Tue, 20 Feb 2024 16:34:15 +0700 Subject: [PATCH 6/8] =?UTF-8?q?=F0=9F=8E=A8=20style:=20add=20new=20shadows?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/frontend/tailwind.config.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/frontend/tailwind.config.js b/packages/frontend/tailwind.config.js index 92ee77d..00ce21f 100644 --- a/packages/frontend/tailwind.config.js +++ b/packages/frontend/tailwind.config.js @@ -144,6 +144,9 @@ export default withMT({ boxShadow: { button: 'inset 0px 0px 4px rgba(255, 255, 255, 0.25), inset 0px -2px 0px rgba(0, 0, 0, 0.04)', + calendar: + '0px 3px 20px rgba(8, 47, 86, 0.1), 0px 0px 4px rgba(8, 47, 86, 0.14)', + field: '0px 1px 2px rgba(0, 0, 0, 0.04)', }, spacing: { 2.5: '0.625rem', From dc6bf3794a143dfc42d1e9dc99ccbc73c6c607f4 Mon Sep 17 00:00:00 2001 From: Wahyu Kurniawan Date: Tue, 20 Feb 2024 16:54:32 +0700 Subject: [PATCH 7/8] =?UTF-8?q?=E2=9A=A1=EF=B8=8F=20feat:=20add=20interact?= =?UTF-8?q?ion=20on=20the=20example=20page?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../frontend/src/pages/components/index.tsx | 28 ++++++++++++++++--- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/packages/frontend/src/pages/components/index.tsx b/packages/frontend/src/pages/components/index.tsx index 4ff2e58..775acbb 100644 --- a/packages/frontend/src/pages/components/index.tsx +++ b/packages/frontend/src/pages/components/index.tsx @@ -1,9 +1,13 @@ import { Button, ButtonOrLinkProps } from 'components/shared/Button'; import { Calendar } from 'components/shared/Calendar'; import { PlusIcon } from 'components/shared/CustomIcon'; -import React from 'react'; +import React, { useState } from 'react'; +import { Value } from 'react-calendar/dist/cjs/shared/types'; const Page = () => { + const [singleDate, setSingleDate] = useState(); + const [dateRange, setDateRange] = useState(); + return (
@@ -75,9 +79,25 @@ const Page = () => {

Calendar

-
- - +
+
+

Selected date: {singleDate?.toString()}

+ +
+
+

+ Start date:{' '} + {dateRange instanceof Array ? dateRange[0]?.toString() : ''}{' '} +
+ End date:{' '} + {dateRange instanceof Array ? dateRange[1]?.toString() : ''} +

+ +
From 5c5d759c10c8d891e854e0052da115ada6daccfd Mon Sep 17 00:00:00 2001 From: Wahyu Kurniawan Date: Tue, 20 Feb 2024 23:08:04 +0700 Subject: [PATCH 8/8] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20refactor:=20fix=20typo?= =?UTF-8?q?=20and=20change=20to=20readable=20name?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../shared/Calendar/Calendar.theme.ts | 4 +-- .../components/shared/Calendar/Calendar.tsx | 30 +++++++++++-------- 2 files changed, 20 insertions(+), 14 deletions(-) diff --git a/packages/frontend/src/components/shared/Calendar/Calendar.theme.ts b/packages/frontend/src/components/shared/Calendar/Calendar.theme.ts index e39e592..ce6e93d 100644 --- a/packages/frontend/src/components/shared/Calendar/Calendar.theme.ts +++ b/packages/frontend/src/components/shared/Calendar/Calendar.theme.ts @@ -17,8 +17,8 @@ export const calendarTheme = tv({ 'py-2.5', 'px-1', ], - dropdowns: ['flex', 'items-center', 'justify-center', 'gap-1.5', 'flex-1'], - dropdown: [ + actions: ['flex', 'items-center', 'justify-center', 'gap-1.5', 'flex-1'], + button: [ 'flex', 'items-center', 'gap-2', diff --git a/packages/frontend/src/components/shared/Calendar/Calendar.tsx b/packages/frontend/src/components/shared/Calendar/Calendar.tsx index 737eee7..dfc15e3 100644 --- a/packages/frontend/src/components/shared/Calendar/Calendar.tsx +++ b/packages/frontend/src/components/shared/Calendar/Calendar.tsx @@ -21,7 +21,7 @@ import { import './Calendar.css'; import { format } from 'date-fns'; -const CALENDAR_VIEW = ['month', 'year', 'decade', 'century'] as const; +const CALENDAR_VIEW = ['month', 'year', 'decade'] as const; export type CalendarView = (typeof CALENDAR_VIEW)[number]; /** @@ -82,12 +82,18 @@ export const Calendar = ({ onChange: onChangeProp, ...props }: CalendarProps): JSX.Element => { - const { wrapper, calendar, navigation, dropdowns, dropdown, footer } = - calendarTheme(); + const { + wrapper, + calendar, + navigation, + actions: actionsClass, + button, + footer, + } = calendarTheme(); const today = new Date(); const currentMonth = format(today, 'MMM'); - const currentyear = format(today, 'yyyy'); + const currentYear = format(today, 'yyyy'); const [view, setView] = useState('month'); const [activeDate, setActiveDate] = useState( @@ -95,7 +101,7 @@ export const Calendar = ({ ); const [value, setValue] = useState(valueProp as Value); const [month, setMonth] = useState(currentMonth); - const [year, setYear] = useState(currentyear); + const [year, setYear] = useState(currentYear); /** * Update the navigation label based on the active date @@ -112,10 +118,10 @@ export const Calendar = ({ * Change the active date base on the action and range */ const handleNavigate = useCallback( - (action: 'previous' | 'next', range: 'month' | 'year' | 'decade') => { + (action: 'previous' | 'next', view: CalendarView) => { setActiveDate((date) => { const newDate = new Date(date); - switch (range) { + switch (view) { case 'month': newDate.setMonth( action === 'previous' ? date.getMonth() - 1 : date.getMonth() + 1, @@ -158,10 +164,10 @@ export const Calendar = ({ * and also update the navigation label */ const handleChangeNavigation = useCallback( - (type: 'month' | 'year', date: Date) => { + (view: 'month' | 'year', date: Date) => { setActiveDate(date); changeNavigationLabel(date); - setView(type); + setView(view); }, [setActiveDate, changeNavigationLabel, setView], ); @@ -225,10 +231,10 @@ export const Calendar = ({ -
+