Skeleton loader for booking widget on desktop

This commit is contained in:
Anton Gunnarsson
2024-11-14 13:24:06 +01:00
parent 3e62df27cc
commit 1980ae4350
9 changed files with 233 additions and 11 deletions

View File

@@ -1,17 +1,11 @@
import { env } from "@/env/server"
import LoadingSpinner from "@/components/LoadingSpinner"
import styles from "./loading.module.css"
import BookingWidgetSkeleton from "@/components/BookingWidget/BookingWidgetSkeleton"
export default function LoadingBookingWidget() {
if (env.HIDE_FOR_NEXT_RELEASE) {
return null
}
return (
<div className={styles.container}>
<LoadingSpinner />
</div>
)
return <BookingWidgetSkeleton />
}

View 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>
)
}

View File

@@ -11,6 +11,7 @@ import {
import { useFormContext, useWatch } from "react-hook-form"
import { useIntl } from "react-intl"
import SkeletonShimmer from "@/components/SkeletonShimmer"
import Caption from "@/components/TempDesignSystem/Text/Caption"
import Input from "../Input"
@@ -203,3 +204,18 @@ export default function Search({ locations }: SearchProps) {
</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>
)
}

View File

@@ -1,4 +1,5 @@
"use client"
import { FormProvider, useForm } from "react-hook-form"
import { useIntl } from "react-intl"
import Checkbox from "@/components/TempDesignSystem/Form/Checkbox"
@@ -78,3 +79,54 @@ export default function Voucher() {
</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>
)
}

View File

@@ -6,6 +6,7 @@ import { useIntl } from "react-intl"
import useDropdownStore from "@/stores/main-menu"
import { ChevronDownSmallIcon } from "@/components/Icons"
import SkeletonShimmer from "@/components/SkeletonShimmer"
import Body from "@/components/TempDesignSystem/Text/Body"
import useClickOutside from "@/hooks/useClickOutside"
import { useHandleKeyUp } from "@/hooks/useHandleKeyUp"
@@ -73,3 +74,13 @@ export default function MyPagesMenu({
</div>
)
}
export function MyPagesMenuSkeleton() {
return (
<MainMenuButton>
<Avatar />
<SkeletonShimmer width="10ch" />
<ChevronDownSmallIcon className={`${styles.chevron}`} color="red" />
</MainMenuButton>
)
}

View File

@@ -10,7 +10,7 @@ import LoginButton from "@/components/LoginButton"
import { getIntl } from "@/i18n"
import Avatar from "../Avatar"
import MyPagesMenu from "../MyPagesMenu"
import MyPagesMenu, { MyPagesMenuSkeleton } from "../MyPagesMenu"
import MyPagesMobileMenu from "../MyPagesMobileMenu"
import styles from "./myPagesMenuWrapper.module.css"
@@ -62,3 +62,12 @@ export default async function MyPagesMenuWrapper() {
</>
)
}
export function MyPagesMenuWrapperSkeleton() {
return (
<div>
<MyPagesMenuSkeleton />
{/* <MyPagesMobileMenuSkeleton /> */}
</div>
)
}

View File

@@ -6,7 +6,9 @@ import { getIntl } from "@/i18n"
import { getLang } from "@/i18n/serverContext"
import MobileMenuWrapper from "./MobileMenuWrapper"
import MyPagesMenuWrapper from "./MyPagesMenuWrapper"
import MyPagesMenuWrapper, {
MyPagesMenuWrapperSkeleton,
} from "./MyPagesMenuWrapper"
import NavigationMenu from "./NavigationMenu"
import styles from "./mainMenu.module.css"
@@ -32,7 +34,7 @@ export default async function MainMenu() {
<Suspense fallback={"Loading nav"}>
<NavigationMenu isMobile={false} />
</Suspense>
<Suspense fallback={"Loading profile"}>
<Suspense fallback={<MyPagesMenuWrapperSkeleton />}>
<MyPagesMenuWrapper />
</Suspense>
<Suspense fallback={"Loading menu"}>

View 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,
}}
/>
)
}

View 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%);
}
}