Skeleton loader for booking widget on desktop
This commit is contained in:
@@ -1,17 +1,11 @@
|
|||||||
import { env } from "@/env/server"
|
import { env } from "@/env/server"
|
||||||
|
|
||||||
import LoadingSpinner from "@/components/LoadingSpinner"
|
import BookingWidgetSkeleton from "@/components/BookingWidget/BookingWidgetSkeleton"
|
||||||
|
|
||||||
import styles from "./loading.module.css"
|
|
||||||
|
|
||||||
export default function LoadingBookingWidget() {
|
export default function LoadingBookingWidget() {
|
||||||
if (env.HIDE_FOR_NEXT_RELEASE) {
|
if (env.HIDE_FOR_NEXT_RELEASE) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return <BookingWidgetSkeleton />
|
||||||
<div className={styles.container}>
|
|
||||||
<LoadingSpinner />
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|||||||
90
components/BookingWidget/BookingWidgetSkeleton.tsx
Normal file
90
components/BookingWidget/BookingWidgetSkeleton.tsx
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
"use client"
|
||||||
|
import { useIntl } from "react-intl"
|
||||||
|
|
||||||
|
import { SearchIcon } from "@/components/Icons"
|
||||||
|
|
||||||
|
import { SearchSkeleton } from "../Forms/BookingWidget/FormContent/Search"
|
||||||
|
import { VoucherSkeleton } from "../Forms/BookingWidget/FormContent/Voucher"
|
||||||
|
import { bookingWidgetVariants } from "../Forms/BookingWidget/variants"
|
||||||
|
import SkeletonShimmer from "../SkeletonShimmer"
|
||||||
|
import Button from "../TempDesignSystem/Button"
|
||||||
|
import Caption from "../TempDesignSystem/Text/Caption"
|
||||||
|
|
||||||
|
import formStyles from "../Forms/BookingWidget/form.module.css"
|
||||||
|
import formContentStyles from "../Forms/BookingWidget/FormContent/formContent.module.css"
|
||||||
|
import widgetStyles from "./bookingWidget.module.css"
|
||||||
|
|
||||||
|
export default function BookingWidgetSkeleton() {
|
||||||
|
const intl = useIntl()
|
||||||
|
|
||||||
|
const classNames = bookingWidgetVariants({
|
||||||
|
type: "full",
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className={widgetStyles.containerDesktop}>
|
||||||
|
<section className={classNames}>
|
||||||
|
<form className={formStyles.form}>
|
||||||
|
<div className={formContentStyles.input}>
|
||||||
|
<div className={formContentStyles.inputContainer}>
|
||||||
|
<div className={formContentStyles.where}>
|
||||||
|
<SearchSkeleton />
|
||||||
|
</div>
|
||||||
|
<div className={formContentStyles.when}>
|
||||||
|
<Caption color="red" type="bold">
|
||||||
|
{intl.formatMessage(
|
||||||
|
{ id: "booking.nights" },
|
||||||
|
{ totalNights: 0 }
|
||||||
|
)}
|
||||||
|
</Caption>
|
||||||
|
<SkeletonShimmer />
|
||||||
|
</div>
|
||||||
|
<div className={formContentStyles.rooms}>
|
||||||
|
<Caption color="red" type="bold" asChild>
|
||||||
|
<span>{intl.formatMessage({ id: "Guests & Rooms" })}</span>
|
||||||
|
</Caption>
|
||||||
|
<SkeletonShimmer />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className={formContentStyles.voucherContainer}>
|
||||||
|
<VoucherSkeleton />
|
||||||
|
</div>
|
||||||
|
<div className={formContentStyles.buttonContainer}>
|
||||||
|
<Button
|
||||||
|
className={formContentStyles.button}
|
||||||
|
intent="primary"
|
||||||
|
theme="base"
|
||||||
|
type="submit"
|
||||||
|
disabled
|
||||||
|
>
|
||||||
|
<Caption
|
||||||
|
color="white"
|
||||||
|
type="bold"
|
||||||
|
className={formContentStyles.buttonText}
|
||||||
|
asChild
|
||||||
|
>
|
||||||
|
<span>{intl.formatMessage({ id: "Search" })}</span>
|
||||||
|
</Caption>
|
||||||
|
<span className={formContentStyles.icon}>
|
||||||
|
<SearchIcon color="white" width={28} height={28} />
|
||||||
|
</span>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</section>
|
||||||
|
{/* <section className={styles.containerMobile} data-open={isOpen}>
|
||||||
|
<button
|
||||||
|
className={styles.close}
|
||||||
|
onClick={closeMobileSearch}
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
<CloseLargeIcon />
|
||||||
|
</button>
|
||||||
|
<Form locations={locations} type={type} />
|
||||||
|
</section>
|
||||||
|
<div className={styles.backdrop} onClick={closeMobileSearch} />
|
||||||
|
<MobileToggleButton openMobileSearch={openMobileSearch} /> */}
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
import { useFormContext, useWatch } from "react-hook-form"
|
import { useFormContext, useWatch } from "react-hook-form"
|
||||||
import { useIntl } from "react-intl"
|
import { useIntl } from "react-intl"
|
||||||
|
|
||||||
|
import SkeletonShimmer from "@/components/SkeletonShimmer"
|
||||||
import Caption from "@/components/TempDesignSystem/Text/Caption"
|
import Caption from "@/components/TempDesignSystem/Text/Caption"
|
||||||
|
|
||||||
import Input from "../Input"
|
import Input from "../Input"
|
||||||
@@ -203,3 +204,18 @@ export default function Search({ locations }: SearchProps) {
|
|||||||
</Downshift>
|
</Downshift>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function SearchSkeleton() {
|
||||||
|
return (
|
||||||
|
<div className={styles.container}>
|
||||||
|
<div className={styles.label}>
|
||||||
|
<Caption type="bold" color="red" asChild>
|
||||||
|
<span>Where to</span>
|
||||||
|
</Caption>
|
||||||
|
</div>
|
||||||
|
<div className={styles.input}>
|
||||||
|
<SkeletonShimmer />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
import { FormProvider, useForm } from "react-hook-form"
|
||||||
import { useIntl } from "react-intl"
|
import { useIntl } from "react-intl"
|
||||||
|
|
||||||
import Checkbox from "@/components/TempDesignSystem/Form/Checkbox"
|
import Checkbox from "@/components/TempDesignSystem/Form/Checkbox"
|
||||||
@@ -78,3 +79,54 @@ export default function Voucher() {
|
|||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function VoucherSkeleton() {
|
||||||
|
const intl = useIntl()
|
||||||
|
|
||||||
|
const vouchers = intl.formatMessage({ id: "Code / Voucher" })
|
||||||
|
const useVouchers = intl.formatMessage({ id: "Use code/voucher" })
|
||||||
|
const addVouchers = intl.formatMessage({ id: "Add code" })
|
||||||
|
const bonus = intl.formatMessage({ id: "Use bonus cheque" })
|
||||||
|
const reward = intl.formatMessage({ id: "Book reward night" })
|
||||||
|
|
||||||
|
const form = useForm()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FormProvider {...form}>
|
||||||
|
<div className={styles.optionsContainer}>
|
||||||
|
<div className={styles.vouchers}>
|
||||||
|
<label>
|
||||||
|
<Caption color="disabled" type="bold" asChild>
|
||||||
|
<span>{vouchers}</span>
|
||||||
|
</Caption>
|
||||||
|
{/* <InfoCircleIcon color="white" className={styles.infoIcon} /> Out of scope for this release */}
|
||||||
|
</label>
|
||||||
|
<Input type="text" placeholder={addVouchers} disabled />
|
||||||
|
</div>
|
||||||
|
<div className={styles.options}>
|
||||||
|
<div className={`${styles.option} ${styles.checkboxVoucher}`}>
|
||||||
|
<Checkbox name="useVouchers" registerOptions={{ disabled: true }}>
|
||||||
|
<Caption color="disabled" asChild>
|
||||||
|
<span>{useVouchers}</span>
|
||||||
|
</Caption>
|
||||||
|
</Checkbox>
|
||||||
|
</div>
|
||||||
|
<div className={styles.option}>
|
||||||
|
<Checkbox name="useBonus" registerOptions={{ disabled: true }}>
|
||||||
|
<Caption color="disabled" asChild>
|
||||||
|
<span>{bonus}</span>
|
||||||
|
</Caption>
|
||||||
|
</Checkbox>
|
||||||
|
</div>
|
||||||
|
<div className={styles.option}>
|
||||||
|
<Checkbox name="useReward" registerOptions={{ disabled: true }}>
|
||||||
|
<Caption color="disabled" asChild>
|
||||||
|
<span>{reward}</span>
|
||||||
|
</Caption>
|
||||||
|
</Checkbox>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</FormProvider>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { useIntl } from "react-intl"
|
|||||||
import useDropdownStore from "@/stores/main-menu"
|
import useDropdownStore from "@/stores/main-menu"
|
||||||
|
|
||||||
import { ChevronDownSmallIcon } from "@/components/Icons"
|
import { ChevronDownSmallIcon } from "@/components/Icons"
|
||||||
|
import SkeletonShimmer from "@/components/SkeletonShimmer"
|
||||||
import Body from "@/components/TempDesignSystem/Text/Body"
|
import Body from "@/components/TempDesignSystem/Text/Body"
|
||||||
import useClickOutside from "@/hooks/useClickOutside"
|
import useClickOutside from "@/hooks/useClickOutside"
|
||||||
import { useHandleKeyUp } from "@/hooks/useHandleKeyUp"
|
import { useHandleKeyUp } from "@/hooks/useHandleKeyUp"
|
||||||
@@ -73,3 +74,13 @@ export default function MyPagesMenu({
|
|||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function MyPagesMenuSkeleton() {
|
||||||
|
return (
|
||||||
|
<MainMenuButton>
|
||||||
|
<Avatar />
|
||||||
|
<SkeletonShimmer width="10ch" />
|
||||||
|
<ChevronDownSmallIcon className={`${styles.chevron}`} color="red" />
|
||||||
|
</MainMenuButton>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import LoginButton from "@/components/LoginButton"
|
|||||||
import { getIntl } from "@/i18n"
|
import { getIntl } from "@/i18n"
|
||||||
|
|
||||||
import Avatar from "../Avatar"
|
import Avatar from "../Avatar"
|
||||||
import MyPagesMenu from "../MyPagesMenu"
|
import MyPagesMenu, { MyPagesMenuSkeleton } from "../MyPagesMenu"
|
||||||
import MyPagesMobileMenu from "../MyPagesMobileMenu"
|
import MyPagesMobileMenu from "../MyPagesMobileMenu"
|
||||||
|
|
||||||
import styles from "./myPagesMenuWrapper.module.css"
|
import styles from "./myPagesMenuWrapper.module.css"
|
||||||
@@ -62,3 +62,12 @@ export default async function MyPagesMenuWrapper() {
|
|||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function MyPagesMenuWrapperSkeleton() {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<MyPagesMenuSkeleton />
|
||||||
|
{/* <MyPagesMobileMenuSkeleton /> */}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
@@ -6,7 +6,9 @@ import { getIntl } from "@/i18n"
|
|||||||
import { getLang } from "@/i18n/serverContext"
|
import { getLang } from "@/i18n/serverContext"
|
||||||
|
|
||||||
import MobileMenuWrapper from "./MobileMenuWrapper"
|
import MobileMenuWrapper from "./MobileMenuWrapper"
|
||||||
import MyPagesMenuWrapper from "./MyPagesMenuWrapper"
|
import MyPagesMenuWrapper, {
|
||||||
|
MyPagesMenuWrapperSkeleton,
|
||||||
|
} from "./MyPagesMenuWrapper"
|
||||||
import NavigationMenu from "./NavigationMenu"
|
import NavigationMenu from "./NavigationMenu"
|
||||||
|
|
||||||
import styles from "./mainMenu.module.css"
|
import styles from "./mainMenu.module.css"
|
||||||
@@ -32,7 +34,7 @@ export default async function MainMenu() {
|
|||||||
<Suspense fallback={"Loading nav"}>
|
<Suspense fallback={"Loading nav"}>
|
||||||
<NavigationMenu isMobile={false} />
|
<NavigationMenu isMobile={false} />
|
||||||
</Suspense>
|
</Suspense>
|
||||||
<Suspense fallback={"Loading profile"}>
|
<Suspense fallback={<MyPagesMenuWrapperSkeleton />}>
|
||||||
<MyPagesMenuWrapper />
|
<MyPagesMenuWrapper />
|
||||||
</Suspense>
|
</Suspense>
|
||||||
<Suspense fallback={"Loading menu"}>
|
<Suspense fallback={"Loading menu"}>
|
||||||
|
|||||||
19
components/SkeletonShimmer/index.tsx
Normal file
19
components/SkeletonShimmer/index.tsx
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import styles from "./skeleton.module.css"
|
||||||
|
|
||||||
|
export default function SkeletonShimmer({
|
||||||
|
height,
|
||||||
|
width,
|
||||||
|
}: {
|
||||||
|
height?: string
|
||||||
|
width?: string
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={styles.shimmer}
|
||||||
|
style={{
|
||||||
|
height: height,
|
||||||
|
width: width,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
29
components/SkeletonShimmer/skeleton.module.css
Normal file
29
components/SkeletonShimmer/skeleton.module.css
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
.shimmer {
|
||||||
|
background-color: hsla(0, 0%, 85%, 0.5);
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
border-radius: 4px;
|
||||||
|
min-height: 1em;
|
||||||
|
}
|
||||||
|
.shimmer::after {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
transform: translateX(-100%);
|
||||||
|
background-image: linear-gradient(
|
||||||
|
90deg,
|
||||||
|
rgba(255, 255, 255, 0) 0,
|
||||||
|
rgba(255, 255, 255, 0.2) 20%,
|
||||||
|
rgba(255, 255, 255, 0.5) 60%,
|
||||||
|
rgba(255, 255, 255, 0) 100%
|
||||||
|
);
|
||||||
|
animation: shimmer 3s infinite;
|
||||||
|
content: "";
|
||||||
|
}
|
||||||
|
@keyframes shimmer {
|
||||||
|
100% {
|
||||||
|
transform: translateX(100%);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user