From 76e47994d02393f625be3414e091cc20397ee5eb Mon Sep 17 00:00:00 2001 From: Simon Emanuelsson Date: Fri, 23 Aug 2024 16:17:35 +0200 Subject: [PATCH] feat: add initial datepicker, no ui/ux --- components/DatePicker/DatePicker.tsx | 62 ++++++++++++++++ components/DatePicker/date-picker.module.css | 31 ++++++++ components/DatePicker/index.tsx | 73 +++++++++++++++++++ .../FormContent/formContent.module.css | 2 +- .../Forms/BookingWidget/FormContent/index.tsx | 17 ++++- components/Forms/BookingWidget/index.tsx | 6 +- components/Forms/BookingWidget/schema.ts | 7 +- i18n/index.ts | 1 - lib/graphql/_request.ts | 18 ++--- package-lock.json | 27 +++++++ package.json | 1 + types/components/datepicker.ts | 3 + 12 files changed, 227 insertions(+), 21 deletions(-) create mode 100644 components/DatePicker/DatePicker.tsx create mode 100644 components/DatePicker/date-picker.module.css create mode 100644 components/DatePicker/index.tsx create mode 100644 types/components/datepicker.ts diff --git a/components/DatePicker/DatePicker.tsx b/components/DatePicker/DatePicker.tsx new file mode 100644 index 000000000..a48bb3831 --- /dev/null +++ b/components/DatePicker/DatePicker.tsx @@ -0,0 +1,62 @@ +"use client" +import { da, de, fi, nb, sv } from "date-fns/locale" +import { useState } from "react" +import { type DateRange, DayPicker } from "react-day-picker" + +import { Lang } from "@/constants/languages" +import { dt } from "@/lib/dt" + +import useLang from "@/hooks/useLang" + +import classNames from "react-day-picker/style.module.css" + +const locales = { + [Lang.da]: da, + [Lang.de]: de, + [Lang.fi]: fi, + [Lang.no]: nb, + [Lang.sv]: sv, +} + +export interface DatePickerProps { + handleOnSelect: (selected: DateRange) => void + initialSelected?: DateRange +} + +export default function DatePicker({ + handleOnSelect, + initialSelected = { + from: undefined, + to: undefined, + }, +}: DatePickerProps) { + const lang = useLang() + const [selectedDate, setSelectedDate] = useState(initialSelected) + + function handleSelectDate(selected: DateRange) { + handleOnSelect(selected) + setSelectedDate(selected) + } + + /** English is default language and doesn't need to be imported */ + const locale = lang === Lang.en ? undefined : locales[lang] + + const currentDate = dt().toDate() + const startOfMonth = dt(currentDate).set("date", 1).toDate() + const yesterday = dt(currentDate).subtract(1, "day").toDate() + return ( + + ) +} diff --git a/components/DatePicker/date-picker.module.css b/components/DatePicker/date-picker.module.css new file mode 100644 index 000000000..926b9ad47 --- /dev/null +++ b/components/DatePicker/date-picker.module.css @@ -0,0 +1,31 @@ +.container { + overflow: hidden; + position: relative; + + &[data-isopen="true"] { + overflow: visible; + } +} + +.hideWrapper { + background-color: var(--Main-Grey-White); + border-radius: var(--Corner-radius-Medium); + box-shadow: 0px 16px 24px 0px rgba(0, 0, 0, 0.08); + padding: var(--Spacing-x-one-and-half); + position: absolute; + /** BookingWidget padding + border-width */ + top: calc(100% + var(--Spacing-x2) + 1px); +} + +.btn { + background: none; + border: none; + cursor: pointer; + outline: none; + padding: 0; + width: 100%; +} + +.body { + opacity: 0.8; +} diff --git a/components/DatePicker/index.tsx b/components/DatePicker/index.tsx new file mode 100644 index 000000000..f7de60e20 --- /dev/null +++ b/components/DatePicker/index.tsx @@ -0,0 +1,73 @@ +"use client" +import { useEffect, useRef, useState } from "react" +import { useFormContext, useWatch } from "react-hook-form" + +import { dt } from "@/lib/dt" + +import Body from "@/components/TempDesignSystem/Text/Body" +import useLang from "@/hooks/useLang" + +import DatePicker from "./DatePicker" + +import styles from "./date-picker.module.css" + +import type { DateRange } from "react-day-picker" + +import type { DatePickerFormProps } from "@/types/components/datepicker" + +export default function DatePickerForm({ name = "date" }: DatePickerFormProps) { + const lang = useLang() + const [isOpen, setIsOpen] = useState(false) + const selectedDate = useWatch({ name }) + const { register, setValue } = useFormContext() + const ref = useRef(null) + + function handleOnClick() { + setIsOpen((prevIsOpen) => !prevIsOpen) + } + + function handleSelectDate(selected: DateRange) { + setValue(name, { + from: dt(selected.from).format("YYYY-MM-DD"), + to: dt(selected.to).format("YYYY-MM-DD"), + }) + } + + useEffect(() => { + function handleClickOutside(evt: Event) { + const target = evt.target as HTMLElement + if (ref.current && target && !ref.current.contains(target)) { + setIsOpen(false) + } + } + + document.addEventListener("click", handleClickOutside) + + return () => { + document.removeEventListener("click", handleClickOutside) + } + }, [setIsOpen]) + + const selectedFromDate = dt(selectedDate.from) + .locale(lang) + .format("ddd D MMM") + const selectedToDate = dt(selectedDate.to).locale(lang).format("ddd D MMM") + + return ( +
+ + + +
+ +
+
+ ) +} diff --git a/components/Forms/BookingWidget/FormContent/formContent.module.css b/components/Forms/BookingWidget/FormContent/formContent.module.css index bacb06b50..eea85e3d3 100644 --- a/components/Forms/BookingWidget/FormContent/formContent.module.css +++ b/components/Forms/BookingWidget/FormContent/formContent.module.css @@ -42,4 +42,4 @@ .option { display: flex; -} +} \ No newline at end of file diff --git a/components/Forms/BookingWidget/FormContent/index.tsx b/components/Forms/BookingWidget/FormContent/index.tsx index 787d32708..e4e86450a 100644 --- a/components/Forms/BookingWidget/FormContent/index.tsx +++ b/components/Forms/BookingWidget/FormContent/index.tsx @@ -1,6 +1,10 @@ "use client" +import { useWatch } from "react-hook-form" import { useIntl } from "react-intl" +import { dt } from "@/lib/dt" + +import DatePicker from "@/components/DatePicker" import Caption from "@/components/TempDesignSystem/Text/Caption" import Search from "./Search" @@ -13,11 +17,15 @@ export default function FormContent({ locations, }: BookingWidgetFormContentProps) { const intl = useIntl() - const when = intl.formatMessage({ id: "When" }) + const selectedDate = useWatch({ name: "date" }) + const rooms = intl.formatMessage({ id: "Rooms & Guests" }) const vouchers = intl.formatMessage({ id: "Booking codes and vouchers" }) const bonus = intl.formatMessage({ id: "Use bonus cheque" }) const reward = intl.formatMessage({ id: "Book reward night" }) + + const nights = dt(selectedDate.to).diff(dt(selectedDate.from), "days") + return (
@@ -25,9 +33,12 @@ export default function FormContent({
- {when} + {nights}{" "} + {nights > 1 + ? intl.formatMessage({ id: "nights" }) + : intl.formatMessage({ id: "night" })} - +
diff --git a/components/Forms/BookingWidget/index.tsx b/components/Forms/BookingWidget/index.tsx index 106eeedf0..153d8417c 100644 --- a/components/Forms/BookingWidget/index.tsx +++ b/components/Forms/BookingWidget/index.tsx @@ -37,11 +37,11 @@ export default function Form({ locations }: BookingWidgetFormProps) { location: sessionStorageSearchData ? encodeURIComponent(sessionStorageSearchData) : undefined, - nights: { + date: { // UTC is required to handle requests from far away timezones https://scandichotels.atlassian.net/browse/SWAP-6375 & PET-507 // This is specifically to handle timezones falling in different dates. - fromDate: dt().utc().format("DD/MM/YYYY"), - toDate: dt().utc().add(1, "day").format("DD/MM/YYYY"), + from: dt().utc().format("YYYY-MM-DD"), + to: dt().utc().add(1, "day").format("YYYY-MM-DD"), }, bookingCode: "", redemption: false, diff --git a/components/Forms/BookingWidget/schema.ts b/components/Forms/BookingWidget/schema.ts index a27182ef9..cccd51a71 100644 --- a/components/Forms/BookingWidget/schema.ts +++ b/components/Forms/BookingWidget/schema.ts @@ -4,10 +4,9 @@ import type { Location } from "@/types/trpc/routers/hotel/locations" export const bookingWidgetSchema = z.object({ search: z.string({ coerce: true }).min(1, "Required"), - nights: z.object({ - // Update this as required once started working with Date picker in Nights component - fromDate: z.string(), - toDate: z.string(), + date: z.object({ + from: z.string(), + to: z.string(), }), location: z.string().refine( (value) => { diff --git a/i18n/index.ts b/i18n/index.ts index 85d04fe20..be2d744bb 100644 --- a/i18n/index.ts +++ b/i18n/index.ts @@ -21,7 +21,6 @@ async function initIntl(lang: Lang) { export async function getIntl(forceLang?: Lang) { const h = headers() let lang = h.get("x-lang") as Lang - if (!lang) { lang = Lang.en } diff --git a/lib/graphql/_request.ts b/lib/graphql/_request.ts index 27178ed7c..8b9e94392 100644 --- a/lib/graphql/_request.ts +++ b/lib/graphql/_request.ts @@ -53,13 +53,13 @@ export async function request( } } - // const print = (await import("graphql/language/printer")).print - // const nr = Math.random() - // console.log(`START REQUEST ${nr}`) - // console.time(`OUTGOING REQUEST ${nr}`) - // console.log(`Sending reqeust to ${env.CMS_URL}`) - // console.log(`Query:`, print(query as DocumentNode)) - // console.log(`Variables:`, variables) + const print = (await import("graphql/language/printer")).print + const nr = Math.random() + console.log(`START REQUEST ${nr}`) + console.time(`OUTGOING REQUEST ${nr}`) + console.log(`Sending reqeust to ${env.CMS_URL}`) + console.log(`Query:`, print(query as DocumentNode)) + console.log(`Variables:`, variables) const response = await client.request({ document: query, @@ -70,8 +70,8 @@ export async function request( variables, }) - // console.timeEnd(`OUTGOING REQUEST ${nr}`) - // console.log({ response }) + console.timeEnd(`OUTGOING REQUEST ${nr}`) + console.log({ response }) return { data: response } } catch (error) { diff --git a/package-lock.json b/package-lock.json index 4d671fa93..35805d69e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -43,6 +43,7 @@ "next": "^14.2.7", "next-auth": "^5.0.0-beta.19", "react": "^18", + "react-day-picker": "^9.0.8", "react-dom": "^18", "react-feather": "^2.0.10", "react-hook-form": "^7.51.2", @@ -8749,6 +8750,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/date-fns": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-3.6.0.tgz", + "integrity": "sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/kossnocorp" + } + }, "node_modules/dayjs": { "version": "1.11.10", "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.10.tgz", @@ -16082,6 +16093,22 @@ "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0" } }, + "node_modules/react-day-picker": { + "version": "9.0.8", + "resolved": "https://registry.npmjs.org/react-day-picker/-/react-day-picker-9.0.8.tgz", + "integrity": "sha512-dZM94abRNnc2jC/wkWn56358GHJcfAHfyC2Th9asyIUQhFIC5D2Ef5qUG9n1b5t8PeCJst7eCWJ6b+XZaAjxhA==", + "license": "MIT", + "dependencies": { + "date-fns": "^3.6.0" + }, + "funding": { + "type": "individual", + "url": "https://github.com/sponsors/gpbl" + }, + "peerDependencies": { + "react": ">=16.8.0" + } + }, "node_modules/react-dom": { "version": "18.2.0", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz", diff --git a/package.json b/package.json index 22f2a0fa7..2be8bd832 100644 --- a/package.json +++ b/package.json @@ -59,6 +59,7 @@ "next": "^14.2.7", "next-auth": "^5.0.0-beta.19", "react": "^18", + "react-day-picker": "^9.0.8", "react-dom": "^18", "react-feather": "^2.0.10", "react-hook-form": "^7.51.2", diff --git a/types/components/datepicker.ts b/types/components/datepicker.ts new file mode 100644 index 000000000..976c792f6 --- /dev/null +++ b/types/components/datepicker.ts @@ -0,0 +1,3 @@ +export interface DatePickerFormProps { + name?: string +}