feat: add desktop ui to calendar

This commit is contained in:
Simon Emanuelsson
2024-08-23 16:17:35 +02:00
parent a5dff7b97d
commit 99b14304d4
17 changed files with 405 additions and 177 deletions
@@ -1,12 +1,11 @@
.container {
background-color: var(--Base-Surface-Primary-light-Normal);
border-top: 1px solid var(--Base-Border-Subtle);
border-bottom: 1px solid var(--Base-Border-Subtle);
padding: var(--Spacing-x2) var(--Spacing-x5);
display: none;
}
@media screen and (max-width: 1367px) {
@media screen and (min-width: 1367px) {
.container {
display: none;
border-bottom: 1px solid var(--Base-Border-Subtle);
border-top: 1px solid var(--Base-Border-Subtle);
display: block;
}
}
+91 -8
View File
@@ -2,14 +2,24 @@
import { da, de, fi, nb, sv } from "date-fns/locale"
import { useState } from "react"
import { type DateRange, DayPicker } from "react-day-picker"
import { useIntl } from "react-intl"
import { Lang } from "@/constants/languages"
import { dt } from "@/lib/dt"
import Button from "@/components/TempDesignSystem/Button"
import Divider from "@/components/TempDesignSystem/Divider"
import Caption from "@/components/TempDesignSystem/Text/Caption"
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
import useLang from "@/hooks/useLang"
import { ChevronLeftIcon } from "../Icons"
import styles from "./date-picker.module.css"
import classNames from "react-day-picker/style.module.css"
import type { DatePickerProps } from "@/types/components/datepicker"
const locales = {
[Lang.da]: da,
[Lang.de]: de,
@@ -18,12 +28,8 @@ const locales = {
[Lang.sv]: sv,
}
export interface DatePickerProps {
handleOnSelect: (selected: DateRange) => void
initialSelected?: DateRange
}
export default function DatePicker({
close,
handleOnSelect,
initialSelected = {
from: undefined,
@@ -31,6 +37,7 @@ export default function DatePicker({
},
}: DatePickerProps) {
const lang = useLang()
const intl = useIntl()
const [selectedDate, setSelectedDate] = useState<DateRange>(initialSelected)
function handleSelectDate(selected: DateRange) {
@@ -40,23 +47,99 @@ export default function DatePicker({
/** 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 (
<DayPicker
classNames={classNames}
classNames={{
...classNames,
caption_label: `${classNames.caption_label} ${styles.captionLabel}`,
day: `${classNames.day} ${styles.day}`,
day_button: `${classNames.day_button} ${styles.dayButton}`,
footer: styles.footer,
month_caption: `${classNames.month_caption} ${styles.monthCaption}`,
months: `${classNames.months} ${styles.months}`,
range_end: styles.rangeEnd,
range_middle: styles.rangeMiddle,
range_start: styles.rangeStart,
week: styles.week,
weekday: `${classNames.weekday} ${styles.weekDay}`,
}}
disabled={{ from: startOfMonth, to: yesterday }}
excludeDisabled
footer
formatters={{
formatWeekdayName(weekday) {
return dt(weekday).locale(lang).format("ddd")
},
}}
lang={lang}
locale={locale}
mode="range"
numberOfMonths={2}
onSelect={handleSelectDate}
pagedNavigation
required
selected={selectedDate}
showWeekNumber
startMonth={currentDate}
weekStartsOn={1}
components={{
Chevron(props) {
return <ChevronLeftIcon {...props} height={20} width={20} />
},
Footer(props) {
return (
<>
<Divider className={styles.divider} color="primaryLightSubtle" />
<footer className={props.className}>
<Button
intent="tertiary"
onPress={close}
size="small"
theme="base"
>
<Caption color="white" textTransform="bold">
{intl.formatMessage({ id: "Select dates" })}
</Caption>
</Button>
</footer>
</>
)
},
MonthCaption(props) {
return (
<div className={props.className}>
<Subtitle asChild type="two">
{props.children}
</Subtitle>
</div>
)
},
Nav(props) {
if (Array.isArray(props.children)) {
const prevButton = props.children?.[0]
const nextButton = props.children?.[1]
return (
<>
{prevButton ? (
<nav
className={`${props.className} ${styles.previousButton}`}
>
{prevButton}
</nav>
) : null}
{nextButton ? (
<nav className={`${props.className} ${styles.nextButton}`}>
{nextButton}
</nav>
) : null}
</>
)
}
return <></>
},
}}
/>
)
}
+124 -5
View File
@@ -9,12 +9,16 @@
.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);
border-radius: var(--Corner-radius-Large);
box-shadow: 0px 0px 14px 6px rgba(0, 0, 0, 0.1);
padding: var(--Spacing-x2) var(--Spacing-x3);
position: absolute;
/** BookingWidget padding + border-width */
top: calc(100% + var(--Spacing-x2) + 1px);
/**
BookingWidget padding +
border-width +
wanted space below booking widget
*/
top: calc(100% + var(--Spacing-x2) + 1px + var(--Spacing-x4));
}
.btn {
@@ -29,3 +33,118 @@
.body {
opacity: 0.8;
}
div.months {
flex-wrap: nowrap;
}
.monthCaption {
justify-content: center;
}
.captionLabel {
text-transform: capitalize;
}
td.day,
td.rangeEnd,
td.rangeStart {
font-family: var(--typography-Body-Bold-fontFamily);
font-size: var(--typography-Body-Bold-fontSize);
font-weight: 500;
letter-spacing: var(--typography-Body-Bold-letterSpacing);
line-height: var(--typography-Body-Bold-lineHeight);
text-decoration: var(--typography-Body-Bold-textDecoration);
}
td.rangeEnd,
td.rangeStart {
background: var(--Base-Background-Primary-Normal);
}
td.rangeEnd[aria-selected="true"]:not([data-outside="true"]) {
border-radius: 0 50% 50% 0;
}
td.rangeStart[aria-selected="true"] {
border-radius: 50% 0 0 50%;
}
td.rangeEnd[aria-selected="true"] button.dayButton:hover,
td.rangeStart[aria-selected="true"] button.dayButton:hover {
background: var(--Primary-Light-On-Surface-Accent);
border-radius: 50%;
}
td.rangeEnd[aria-selected="true"]:not([data-outside="true"]) button.dayButton,
td.rangeStart[aria-selected="true"]:not([data-outside="true"])
button.dayButton {
background: var(--Primary-Light-On-Surface-Accent);
border: none;
color: var(--Base-Button-Inverted-Fill-Normal);
}
td.day,
td.day[data-today="true"] {
color: var(--UI-Text-High-contrast);
height: 40px;
padding: var(--Spacing-x-half);
width: 40px;
}
td.day button.dayButton:hover {
background: var(--Base-Surface-Secondary-light-Hover);
}
td.day[data-outside="true"] button.dayButton {
border: none;
}
td.day:not(td.rangeEnd, td.rangeStart)[aria-selected="true"],
td.rangeMiddle[aria-selected="true"] button.dayButton {
background: var(--Base-Background-Primary-Normal);
border: none;
border-radius: 0;
}
td.day[data-disabled="true"],
td.day[data-disabled="true"] button.dayButton,
td.day[data-outside="true"] ~ td.day[data-disabled="true"],
td.day[data-outside="true"] ~ td.day[data-disabled="true"] button.dayButton,
.week:has(td.day[data-outside="true"] ~ td.day[data-disabled="true"])
td.day[data-outside="true"]
button.dayButton {
background: none;
color: var(--Base-Text-Disabled);
cursor: not-allowed;
}
.weekDay {
color: var(--UI-Text-Placeholder);
font-family: var(--typography-Footnote-Labels-fontFamily);
font-size: var(--typography-Footnote-Labels-fontSize);
font-weight: var(--typography-Footnote-Labels-fontWeight);
letter-spacing: var(--typography-Footnote-Labels-letterSpacing);
line-height: var(--typography-Footnote-Labels-lineHeight);
text-decoration: var(--typography-Footnote-Labels-textDecoration);
text-transform: uppercase;
}
.footer {
display: flex;
justify-content: flex-end;
margin-top: var(--Spacing-x2);
}
.divider {
margin-top: var(--Spacing-x2);
}
.nextButton {
transform: rotate(180deg);
right: 0;
}
.previousButton {
left: 0;
}
+5
View File
@@ -22,6 +22,10 @@ export default function DatePickerForm({ name = "date" }: DatePickerFormProps) {
const { register, setValue } = useFormContext()
const ref = useRef<HTMLDivElement | null>(null)
function close() {
setIsOpen(false)
}
function handleOnClick() {
setIsOpen((prevIsOpen) => !prevIsOpen)
}
@@ -64,6 +68,7 @@ export default function DatePickerForm({ name = "date" }: DatePickerFormProps) {
<input {...register("date.to")} type="hidden" />
<div aria-modal className={styles.hideWrapper} role="dialog">
<DatePicker
close={close}
handleOnSelect={handleSelectDate}
initialSelected={selectedDate}
/>
@@ -30,6 +30,7 @@
height: 24px;
outline: none;
position: relative;
width: 100%;
z-index: 2;
}
@@ -33,10 +33,10 @@ export default function FormContent({
</div>
<div className={styles.when}>
<Caption color="red" textTransform="bold">
{nights}{" "}
{nights > 1
? intl.formatMessage({ id: "nights" })
: intl.formatMessage({ id: "night" })}
{intl.formatMessage(
{ id: "booking.nights" },
{ totalNights: nights }
)}
</Caption>
<DatePicker />
</div>
+4 -3
View File
@@ -3,8 +3,9 @@ import { z } from "zod"
import type { Location } from "@/types/trpc/routers/hotel/locations"
export const bookingWidgetSchema = z.object({
search: z.string({ coerce: true }).min(1, "Required"),
bookingCode: z.string(), // Update this as required when working with booking codes component
date: z.object({
// Update this as required once started working with Date picker in Nights component
from: z.string(),
to: z.string(),
}),
@@ -23,9 +24,7 @@ export const bookingWidgetSchema = z.object({
},
{ message: "Required" }
),
bookingCode: z.string(), // Update this as required when working with booking codes component
redemption: z.boolean().default(false),
voucher: z.boolean().default(false),
rooms: z.array(
// This will be updated when working in guests component
z.object({
@@ -38,4 +37,6 @@ export const bookingWidgetSchema = z.object({
),
})
),
search: z.string({ coerce: true }).min(1, "Required"),
voucher: z.boolean().default(false),
})
@@ -69,19 +69,19 @@ a.default {
}
/* SIZES */
.small {
.btn.small {
gap: var(--Spacing-x-quarter);
height: 40px;
padding: var(--Spacing-x1) var(--Spacing-x2);
}
.medium {
.btn.medium {
gap: var(--Spacing-x-half);
height: 48px;
padding: var(--Spacing-x-one-and-half) var(--Spacing-x2);
}
.large {
.btn.large {
gap: var(--Spacing-x-half);
height: 56px;
padding: var(--Spacing-x2) var(--Spacing-x3);
@@ -170,19 +170,19 @@ a.default {
fill: var(--Base-Button-Secondary-On-Fill-Disabled);
}
.baseTertiary {
.btn.baseTertiary {
background-color: var(--Base-Button-Tertiary-Fill-Normal);
color: var(--Base-Button-Tertiary-On-Fill-Normal);
}
.baseTertiary:active,
.baseTertiary:focus,
.baseTertiary:hover {
.btn.baseTertiary:active,
.btn.baseTertiary:focus,
.btn.baseTertiary:hover {
background-color: var(--Base-Button-Tertiary-Fill-Hover);
color: var(--Base-Button-Tertiary-On-Fill-Hover);
}
.baseTertiary:disabled {
.btn.baseTertiary:disabled {
background-color: var(--Base-Button-Tertiary-Fill-Disabled);
color: var(--Base-Button-Tertiary-On-Fill-Disabled);
}
@@ -800,4 +800,4 @@ a.default {
.icon.tertiaryLightSecondary:disabled svg,
.icon.tertiaryLightSecondary:disabled svg * {
fill: var(--Tertiary-Light-Button-Secondary-On-Fill-Disabled);
}
}
@@ -33,6 +33,10 @@
border-bottom-color: var(--Base-Border-Subtle);
}
.primaryLightSubtle {
border-bottom-color: var(--Primary-Light-On-Surface-Divider-subtle);
}
.opacity100 {
opacity: 1;
}
@@ -5,12 +5,13 @@ import styles from "./divider.module.css"
export const dividerVariants = cva(styles.divider, {
variants: {
color: {
burgundy: styles.burgundy,
peach: styles.peach,
beige: styles.beige,
white: styles.white,
subtle: styles.subtle,
burgundy: styles.burgundy,
pale: styles.pale,
peach: styles.peach,
primaryLightSubtle: styles.primaryLightSubtle,
subtle: styles.subtle,
white: styles.white,
},
opacity: {
100: styles.opacity100,