diff --git a/apps/scandic-web/components/Blocks/DynamicContent/Stays/NextStay/NextStayContent.tsx b/apps/scandic-web/components/Blocks/DynamicContent/Stays/NextStay/NextStayContent.tsx index 91ee95bc6..a0090f04f 100644 --- a/apps/scandic-web/components/Blocks/DynamicContent/Stays/NextStay/NextStayContent.tsx +++ b/apps/scandic-web/components/Blocks/DynamicContent/Stays/NextStay/NextStayContent.tsx @@ -139,11 +139,17 @@ export default async function NextStayContent({ color="Inverted" size="Medium" href={bookingUrl} + className={styles.cta} > {intl.formatMessage({ id: "nextStay.seeDetailsAndExtras", defaultMessage: "See details & extras", })} + ) : null} diff --git a/apps/scandic-web/components/Blocks/DynamicContent/Stays/NextStay/index.tsx b/apps/scandic-web/components/Blocks/DynamicContent/Stays/NextStay/index.tsx index 3ae28723d..e3c404a5e 100644 --- a/apps/scandic-web/components/Blocks/DynamicContent/Stays/NextStay/index.tsx +++ b/apps/scandic-web/components/Blocks/DynamicContent/Stays/NextStay/index.tsx @@ -5,7 +5,7 @@ import { Section } from "@/components/Section" import { SectionHeader } from "@/components/Section/Header" import SectionLink from "@/components/Section/Link" -import EmptyUpcomingStaysBlock from "../EmptyUpcomingStays" +import EmptyUpcomingStays from "../Upcoming/EmptyUpcomingStays" import NextStayContent from "./NextStayContent" import styles from "./nextStay.module.css" @@ -17,7 +17,7 @@ export default async function NextStay({ title, link }: NextStayProps) { const nextStay = await caller.user.stays.next() if (!nextStay) { - return env.NEW_STAYS_ON_MY_PAGES ? : null + return env.NEW_STAYS_ON_MY_PAGES ? : null } return ( diff --git a/apps/scandic-web/components/Blocks/DynamicContent/Stays/NextStay/nextStay.module.css b/apps/scandic-web/components/Blocks/DynamicContent/Stays/NextStay/nextStay.module.css index c545f9b09..1194da58d 100644 --- a/apps/scandic-web/components/Blocks/DynamicContent/Stays/NextStay/nextStay.module.css +++ b/apps/scandic-web/components/Blocks/DynamicContent/Stays/NextStay/nextStay.module.css @@ -119,6 +119,10 @@ grid-area: actions; } +.cta { + width: 100%; +} + @media (min-width: 768px) { .nextStayCard { max-width: 100%; diff --git a/apps/scandic-web/components/Blocks/DynamicContent/Stays/OldStayCard/index.tsx b/apps/scandic-web/components/Blocks/DynamicContent/Stays/OldStayCard/index.tsx new file mode 100644 index 000000000..9cf25e2ef --- /dev/null +++ b/apps/scandic-web/components/Blocks/DynamicContent/Stays/OldStayCard/index.tsx @@ -0,0 +1,77 @@ +"use client" + +import { dt } from "@scandic-hotels/common/dt" +import Caption from "@scandic-hotels/design-system/Caption" +import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon" +import Image from "@scandic-hotels/design-system/Image" +import Link from "@scandic-hotels/design-system/OldDSLink" +import Title from "@scandic-hotels/design-system/Title" + +import useLang from "@/hooks/useLang" + +import styles from "./stay.module.css" + +import type { StayCardProps } from "@/types/components/myPages/stays/stayCard" + +export default function OldStayCard({ stay }: StayCardProps) { + const { bookingUrl, isWebAppOrigin } = stay.attributes + + const shouldLinkToMyStay = isWebAppOrigin + + if (!shouldLinkToMyStay) { + return + } + + return ( + + + + ) +} + +function CardContent({ stay }: StayCardProps) { + const lang = useLang() + + const { checkinDate, checkoutDate, hotelInformation } = stay.attributes + + const arrival = dt(checkinDate).locale(lang) + const arrivalDate = arrival.format("DD MMM") + const arrivalDateTime = arrival.format("YYYY-MM-DD") + const depart = dt(checkoutDate).locale(lang) + const departDate = depart.format("DD MMM YYYY") + const departDateTime = depart.format("YYYY-MM-DD") + + return ( +
+ { +
+ + {hotelInformation.hotelName} + +
+ + + + + {/* eslint-disable-next-line formatjs/no-literal-string-in-jsx */} + {" - "} + + + +
+
+
+ ) +} diff --git a/apps/scandic-web/components/Blocks/DynamicContent/Stays/StayCard/stay.module.css b/apps/scandic-web/components/Blocks/DynamicContent/Stays/OldStayCard/stay.module.css similarity index 100% rename from apps/scandic-web/components/Blocks/DynamicContent/Stays/StayCard/stay.module.css rename to apps/scandic-web/components/Blocks/DynamicContent/Stays/OldStayCard/stay.module.css diff --git a/apps/scandic-web/components/Blocks/DynamicContent/Stays/Previous/Card/index.tsx b/apps/scandic-web/components/Blocks/DynamicContent/Stays/Previous/Card/index.tsx deleted file mode 100644 index 82da16409..000000000 --- a/apps/scandic-web/components/Blocks/DynamicContent/Stays/Previous/Card/index.tsx +++ /dev/null @@ -1,105 +0,0 @@ -"use client" - -import Link from "next/link" -import { useIntl } from "react-intl" - -import { dt } from "@scandic-hotels/common/dt" -import { Divider } from "@scandic-hotels/design-system/Divider" -import Image from "@scandic-hotels/design-system/Image" -import ImageFallback from "@scandic-hotels/design-system/ImageFallback" -import { Typography } from "@scandic-hotels/design-system/Typography" - -import useLang from "@/hooks/useLang" -import { getRelativePastTime } from "@/utils/getRelativePastTime" - -import styles from "./card.module.css" - -import type { StayCardProps } from "@/types/components/myPages/stays/stayCard" - -export function Card({ stay }: StayCardProps) { - const { bookingUrl, isWebAppOrigin: shouldLinkToMyStay } = stay.attributes - - if (!shouldLinkToMyStay) { - return - } - - return ( - - - - ) -} - -function CardContent({ stay }: StayCardProps) { - const lang = useLang() - const intl = useIntl() - - const { checkinDate, checkoutDate, hotelInformation } = stay.attributes - - const arrival = dt(checkinDate).locale(lang) - const arrivalDate = arrival.format("DD MMM") - const arrivalDateTime = arrival.format("YYYY-MM-DD") - const depart = dt(checkoutDate).locale(lang) - const departDate = depart.format("DD MMM YYYY") - const departDateTime = depart.format("YYYY-MM-DD") - - const relativeTime = getRelativePastTime(checkoutDate, intl) - - return ( -
- {hotelInformation.hotelContent.images.src ? ( - { - ) : ( - - )} -
-
- -

{hotelInformation.hotelName}

-
- - {hotelInformation.cityName && ( - -

{hotelInformation.cityName}

-
- )} -
- - - -
-
- - {relativeTime} - -
- -
- - {/* eslint-disable-next-line formatjs/no-literal-string-in-jsx */} - - -
-
-
-
-
- ) -} diff --git a/apps/scandic-web/components/Blocks/DynamicContent/Stays/Previous/SeeAllCard/index.tsx b/apps/scandic-web/components/Blocks/DynamicContent/Stays/Previous/Cards/SeeAllCard/index.tsx similarity index 100% rename from apps/scandic-web/components/Blocks/DynamicContent/Stays/Previous/SeeAllCard/index.tsx rename to apps/scandic-web/components/Blocks/DynamicContent/Stays/Previous/Cards/SeeAllCard/index.tsx diff --git a/apps/scandic-web/components/Blocks/DynamicContent/Stays/Previous/SeeAllCard/seeAllCard.module.css b/apps/scandic-web/components/Blocks/DynamicContent/Stays/Previous/Cards/SeeAllCard/seeAllCard.module.css similarity index 100% rename from apps/scandic-web/components/Blocks/DynamicContent/Stays/Previous/SeeAllCard/seeAllCard.module.css rename to apps/scandic-web/components/Blocks/DynamicContent/Stays/Previous/Cards/SeeAllCard/seeAllCard.module.css diff --git a/apps/scandic-web/components/Blocks/DynamicContent/Stays/Previous/cards.module.css b/apps/scandic-web/components/Blocks/DynamicContent/Stays/Previous/Cards/cards.module.css similarity index 100% rename from apps/scandic-web/components/Blocks/DynamicContent/Stays/Previous/cards.module.css rename to apps/scandic-web/components/Blocks/DynamicContent/Stays/Previous/Cards/cards.module.css diff --git a/apps/scandic-web/components/Blocks/DynamicContent/Stays/Previous/Cards.tsx b/apps/scandic-web/components/Blocks/DynamicContent/Stays/Previous/Cards/index.tsx similarity index 77% rename from apps/scandic-web/components/Blocks/DynamicContent/Stays/Previous/Cards.tsx rename to apps/scandic-web/components/Blocks/DynamicContent/Stays/Previous/Cards/index.tsx index c5c8afa65..d82c3ffc7 100644 --- a/apps/scandic-web/components/Blocks/DynamicContent/Stays/Previous/Cards.tsx +++ b/apps/scandic-web/components/Blocks/DynamicContent/Stays/Previous/Cards/index.tsx @@ -2,10 +2,10 @@ import { useState } from "react" -import ListContainer from "../ListContainer" -import { Card } from "./Card" -import { INITIAL_STAYS_FETCH_LIMIT } from "./data" -import { PreviousStaysSidePeek } from "./PreviousStaysSidePeek" +import ListContainer from "../../ListContainer" +import { StayCard } from "../../StayCard" +import { INITIAL_STAYS_FETCH_LIMIT } from "../data" +import { PreviousStaysSidePeek } from "../SidePeek" import { SeeAllCard } from "./SeeAllCard" import styles from "./cards.module.css" @@ -25,7 +25,7 @@ export function Cards({ initialPreviousStays }: PreviousStaysClientProps) {
{visibleStays.map((stay) => ( - + ))} {hasMoreStays && setIsSidePeekOpen(true)} />}
diff --git a/apps/scandic-web/components/Blocks/DynamicContent/Stays/Previous/Client.tsx b/apps/scandic-web/components/Blocks/DynamicContent/Stays/Previous/OldClient.tsx similarity index 92% rename from apps/scandic-web/components/Blocks/DynamicContent/Stays/Previous/Client.tsx rename to apps/scandic-web/components/Blocks/DynamicContent/Stays/Previous/OldClient.tsx index b1e264eb3..5c64f6a6a 100644 --- a/apps/scandic-web/components/Blocks/DynamicContent/Stays/Previous/Client.tsx +++ b/apps/scandic-web/components/Blocks/DynamicContent/Stays/Previous/OldClient.tsx @@ -7,8 +7,8 @@ import Grids from "@/components/TempDesignSystem/Grids" import useLang from "@/hooks/useLang" import ListContainer from "../ListContainer" +import OldStayCard from "../OldStayCard" import ShowMoreButton from "../ShowMoreButton" -import StayCard from "../StayCard" import type { PreviousStaysClientProps, @@ -54,7 +54,7 @@ export function ClientPreviousStays({ {stays.map((stay) => ( - + ))} {hasNextPage ? ( diff --git a/apps/scandic-web/components/Blocks/DynamicContent/Stays/Previous/PreviousStaysSidePeek/index.tsx b/apps/scandic-web/components/Blocks/DynamicContent/Stays/Previous/SidePeek/index.tsx similarity index 96% rename from apps/scandic-web/components/Blocks/DynamicContent/Stays/Previous/PreviousStaysSidePeek/index.tsx rename to apps/scandic-web/components/Blocks/DynamicContent/Stays/Previous/SidePeek/index.tsx index aaf2d7b83..f5875c236 100644 --- a/apps/scandic-web/components/Blocks/DynamicContent/Stays/Previous/PreviousStaysSidePeek/index.tsx +++ b/apps/scandic-web/components/Blocks/DynamicContent/Stays/Previous/SidePeek/index.tsx @@ -12,8 +12,8 @@ import { trpc } from "@scandic-hotels/trpc/client" import useLang from "@/hooks/useLang" import ShowMoreButton from "../../ShowMoreButton" -import { Card } from "../Card" -import { groupStaysByYear } from "../utils/groupStaysByYear" +import { StayCard } from "../../StayCard" +import { groupStaysByYear } from "../../utils/groupStaysByYear" import styles from "./previousStaysSidePeek.module.css" @@ -85,7 +85,7 @@ export function PreviousStaysSidePeek({
{stays.map((stay) => ( - diff --git a/apps/scandic-web/components/Blocks/DynamicContent/Stays/Previous/PreviousStaysSidePeek/previousStaysSidePeek.module.css b/apps/scandic-web/components/Blocks/DynamicContent/Stays/Previous/SidePeek/previousStaysSidePeek.module.css similarity index 100% rename from apps/scandic-web/components/Blocks/DynamicContent/Stays/Previous/PreviousStaysSidePeek/previousStaysSidePeek.module.css rename to apps/scandic-web/components/Blocks/DynamicContent/Stays/Previous/SidePeek/previousStaysSidePeek.module.css diff --git a/apps/scandic-web/components/Blocks/DynamicContent/Stays/Previous/index.tsx b/apps/scandic-web/components/Blocks/DynamicContent/Stays/Previous/index.tsx index 4dc692448..b37d843dc 100644 --- a/apps/scandic-web/components/Blocks/DynamicContent/Stays/Previous/index.tsx +++ b/apps/scandic-web/components/Blocks/DynamicContent/Stays/Previous/index.tsx @@ -7,8 +7,8 @@ import { SectionHeader } from "@/components/Section/Header" import SectionLink from "@/components/Section/Link" import { Cards } from "./Cards" -import { ClientPreviousStays } from "./Client" import { INITIAL_STAYS_FETCH_LIMIT } from "./data" +import { ClientPreviousStays } from "./OldClient" import styles from "./previous.module.css" diff --git a/apps/scandic-web/components/Blocks/DynamicContent/Stays/StayCard/index.tsx b/apps/scandic-web/components/Blocks/DynamicContent/Stays/StayCard/index.tsx index aaf67823f..85d182bbf 100644 --- a/apps/scandic-web/components/Blocks/DynamicContent/Stays/StayCard/index.tsx +++ b/apps/scandic-web/components/Blocks/DynamicContent/Stays/StayCard/index.tsx @@ -1,22 +1,23 @@ "use client" +import Link from "next/link" +import { useIntl } from "react-intl" + import { dt } from "@scandic-hotels/common/dt" -import Caption from "@scandic-hotels/design-system/Caption" -import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon" +import { Divider } from "@scandic-hotels/design-system/Divider" import Image from "@scandic-hotels/design-system/Image" -import Link from "@scandic-hotels/design-system/OldDSLink" -import Title from "@scandic-hotels/design-system/Title" +import ImageFallback from "@scandic-hotels/design-system/ImageFallback" +import { Typography } from "@scandic-hotels/design-system/Typography" import useLang from "@/hooks/useLang" +import { getTimeAgoText } from "@/utils/getTimeAgoText" -import styles from "./stay.module.css" +import styles from "./stayCard.module.css" import type { StayCardProps } from "@/types/components/myPages/stays/stayCard" -export default function StayCard({ stay }: StayCardProps) { - const { bookingUrl, isWebAppOrigin } = stay.attributes - - const shouldLinkToMyStay = isWebAppOrigin +export function StayCard({ stay }: StayCardProps) { + const { bookingUrl, isWebAppOrigin: shouldLinkToMyStay } = stay.attributes if (!shouldLinkToMyStay) { return @@ -31,6 +32,7 @@ export default function StayCard({ stay }: StayCardProps) { function CardContent({ stay }: StayCardProps) { const lang = useLang() + const intl = useIntl() const { checkinDate, checkoutDate, hotelInformation } = stay.attributes @@ -41,37 +43,65 @@ function CardContent({ stay }: StayCardProps) { const departDate = depart.format("DD MMM YYYY") const departDateTime = depart.format("YYYY-MM-DD") + const timeAgoText = getTimeAgoText(checkoutDate, intl) + return ( -
- { -
- - {hotelInformation.hotelName} - -
- - - - - {/* eslint-disable-next-line formatjs/no-literal-string-in-jsx */} - {" - "} - - - +
+ {hotelInformation.hotelContent.images.src ? ( + { + ) : ( + + )} +
+
+ +

{hotelInformation.hotelName}

+
+ + {hotelInformation.cityName && ( + +

{hotelInformation.cityName}

+
+ )}
-
+ + + +
+ {timeAgoText ? ( +
+ + {timeAgoText} + +
+ ) : null} + +
+ + {/* eslint-disable-next-line formatjs/no-literal-string-in-jsx */} + + +
+
+
+
) } diff --git a/apps/scandic-web/components/Blocks/DynamicContent/Stays/Previous/Card/card.module.css b/apps/scandic-web/components/Blocks/DynamicContent/Stays/StayCard/stayCard.module.css similarity index 100% rename from apps/scandic-web/components/Blocks/DynamicContent/Stays/Previous/Card/card.module.css rename to apps/scandic-web/components/Blocks/DynamicContent/Stays/StayCard/stayCard.module.css diff --git a/apps/scandic-web/components/Blocks/DynamicContent/Stays/UpcomingStays/CarouselCard/carouselCard.module.css b/apps/scandic-web/components/Blocks/DynamicContent/Stays/Upcoming/Carousel/CarouselCard/carouselCard.module.css similarity index 100% rename from apps/scandic-web/components/Blocks/DynamicContent/Stays/UpcomingStays/CarouselCard/carouselCard.module.css rename to apps/scandic-web/components/Blocks/DynamicContent/Stays/Upcoming/Carousel/CarouselCard/carouselCard.module.css diff --git a/apps/scandic-web/components/Blocks/DynamicContent/Stays/UpcomingStays/CarouselCard/index.tsx b/apps/scandic-web/components/Blocks/DynamicContent/Stays/Upcoming/Carousel/CarouselCard/index.tsx similarity index 97% rename from apps/scandic-web/components/Blocks/DynamicContent/Stays/UpcomingStays/CarouselCard/index.tsx rename to apps/scandic-web/components/Blocks/DynamicContent/Stays/Upcoming/Carousel/CarouselCard/index.tsx index 36008404d..a060a3315 100644 --- a/apps/scandic-web/components/Blocks/DynamicContent/Stays/UpcomingStays/CarouselCard/index.tsx +++ b/apps/scandic-web/components/Blocks/DynamicContent/Stays/Upcoming/Carousel/CarouselCard/index.tsx @@ -9,10 +9,9 @@ import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon" import Image from "@scandic-hotels/design-system/Image" import { Typography } from "@scandic-hotels/design-system/Typography" +import { getDaysUntilText } from "@/components/Blocks/DynamicContent/Stays/utils/getDaysUntilText" import useLang from "@/hooks/useLang" -import { getDaysUntilText } from "../../utils/getDaysUntilText" - import styles from "./carouselCard.module.css" import type { Stay } from "@scandic-hotels/trpc/routers/user/output" diff --git a/apps/scandic-web/components/Blocks/DynamicContent/Stays/Upcoming/Carousel/SeeAllCard/index.tsx b/apps/scandic-web/components/Blocks/DynamicContent/Stays/Upcoming/Carousel/SeeAllCard/index.tsx new file mode 100644 index 000000000..8cf590234 --- /dev/null +++ b/apps/scandic-web/components/Blocks/DynamicContent/Stays/Upcoming/Carousel/SeeAllCard/index.tsx @@ -0,0 +1,30 @@ +"use client" + +import { useIntl } from "react-intl" + +import { Button } from "@scandic-hotels/design-system/Button" +import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon" + +import styles from "./seeAllCard.module.css" + +interface SeeAllCardProps { + onPress: () => void +} + +export function SeeAllCard({ onPress }: SeeAllCardProps) { + const intl = useIntl() + + return ( +
+ +
+ ) +} diff --git a/apps/scandic-web/components/Blocks/DynamicContent/Stays/Upcoming/Carousel/SeeAllCard/seeAllCard.module.css b/apps/scandic-web/components/Blocks/DynamicContent/Stays/Upcoming/Carousel/SeeAllCard/seeAllCard.module.css new file mode 100644 index 000000000..bddc2753c --- /dev/null +++ b/apps/scandic-web/components/Blocks/DynamicContent/Stays/Upcoming/Carousel/SeeAllCard/seeAllCard.module.css @@ -0,0 +1,11 @@ +.card { + display: flex; + flex-direction: column; + background: var(--Surface-Secondary-Default); + border: 1px solid var(--Border-Default); + overflow: hidden; + border-radius: var(--Corner-radius-lg); + align-items: center; + justify-content: center; + height: 100%; +} diff --git a/apps/scandic-web/components/Blocks/DynamicContent/Stays/Upcoming/Carousel/index.tsx b/apps/scandic-web/components/Blocks/DynamicContent/Stays/Upcoming/Carousel/index.tsx new file mode 100644 index 000000000..b5da10783 --- /dev/null +++ b/apps/scandic-web/components/Blocks/DynamicContent/Stays/Upcoming/Carousel/index.tsx @@ -0,0 +1,89 @@ +"use client" + +import { useState } from "react" + +import { LoadingSpinner } from "@scandic-hotels/design-system/LoadingSpinner" +import { trpc } from "@scandic-hotels/trpc/client" + +import { Carousel } from "@/components/Carousel" +import useLang from "@/hooks/useLang" + +import { UpcomingStaysSidePeek } from "../SidePeek" +import CarouselCard from "./CarouselCard" +import { SeeAllCard } from "./SeeAllCard" + +import styles from "../upcoming.module.css" + +import type { + UpcomingStaysClientProps, + UpcomingStaysNonNullResponseObject, +} from "@/types/components/myPages/stays/upcoming" + +const MAX_VISIBLE_STAYS = 5 + +export default function UpcomingStaysCarousel({ + initialUpcomingStays, +}: UpcomingStaysClientProps) { + const lang = useLang() + const [isSidePeekOpen, setIsSidePeekOpen] = useState(false) + + const { data, isLoading } = trpc.user.stays.upcoming.useInfiniteQuery( + { + limit: 6, + lang, + }, + { + getNextPageParam: (lastPage) => { + return lastPage?.nextCursor + }, + initialData: { + pageParams: [undefined, 1], + pages: [initialUpcomingStays], + }, + } + ) + + if (isLoading) { + return + } + + const stays = data.pages + .filter((page): page is UpcomingStaysNonNullResponseObject => !!page?.data) + .flatMap((page) => page.data) + + if (!stays.length) { + return null + } + + const visibleStays = stays.slice(0, MAX_VISIBLE_STAYS) + const hasMoreStays = stays.length >= MAX_VISIBLE_STAYS + + return ( + <> + + + {visibleStays.map((stay) => ( + + + + ))} + {hasMoreStays && ( + + setIsSidePeekOpen(true)} /> + + )} + + + + + + setIsSidePeekOpen(false)} + /> + + ) +} diff --git a/apps/scandic-web/components/Blocks/DynamicContent/Stays/EmptyUpcomingStays/emptyUpcomingStays.module.css b/apps/scandic-web/components/Blocks/DynamicContent/Stays/Upcoming/EmptyUpcomingStays/emptyUpcomingStays.module.css similarity index 100% rename from apps/scandic-web/components/Blocks/DynamicContent/Stays/EmptyUpcomingStays/emptyUpcomingStays.module.css rename to apps/scandic-web/components/Blocks/DynamicContent/Stays/Upcoming/EmptyUpcomingStays/emptyUpcomingStays.module.css diff --git a/apps/scandic-web/components/Blocks/DynamicContent/Stays/EmptyUpcomingStays/index.tsx b/apps/scandic-web/components/Blocks/DynamicContent/Stays/Upcoming/EmptyUpcomingStays/index.tsx similarity index 97% rename from apps/scandic-web/components/Blocks/DynamicContent/Stays/EmptyUpcomingStays/index.tsx rename to apps/scandic-web/components/Blocks/DynamicContent/Stays/Upcoming/EmptyUpcomingStays/index.tsx index e5dfc53ca..a0f2697a0 100644 --- a/apps/scandic-web/components/Blocks/DynamicContent/Stays/EmptyUpcomingStays/index.tsx +++ b/apps/scandic-web/components/Blocks/DynamicContent/Stays/Upcoming/EmptyUpcomingStays/index.tsx @@ -11,7 +11,7 @@ import { getLang } from "@/i18n/serverContext" import styles from "./emptyUpcomingStays.module.css" -export default async function EmptyUpcomingStaysBlock() { +export default async function EmptyUpcomingStays() { const intl = await getIntl() const lang = await getLang() diff --git a/apps/scandic-web/components/Blocks/DynamicContent/Stays/UpcomingStays/Client.tsx b/apps/scandic-web/components/Blocks/DynamicContent/Stays/Upcoming/OldClient.tsx similarity index 92% rename from apps/scandic-web/components/Blocks/DynamicContent/Stays/UpcomingStays/Client.tsx rename to apps/scandic-web/components/Blocks/DynamicContent/Stays/Upcoming/OldClient.tsx index 9c6c8298c..73f02ebf1 100644 --- a/apps/scandic-web/components/Blocks/DynamicContent/Stays/UpcomingStays/Client.tsx +++ b/apps/scandic-web/components/Blocks/DynamicContent/Stays/Upcoming/OldClient.tsx @@ -7,8 +7,8 @@ import Grids from "@/components/TempDesignSystem/Grids" import useLang from "@/hooks/useLang" import ListContainer from "../ListContainer" +import OldStayCard from "../OldStayCard" import ShowMoreButton from "../ShowMoreButton" -import StayCard from "../StayCard" import type { UpcomingStaysClientProps, @@ -54,7 +54,7 @@ export default function ClientUpcomingStays({ {stays.map((stay) => ( - + ))} {hasNextPage ? ( diff --git a/apps/scandic-web/components/Blocks/DynamicContent/Stays/Upcoming/SidePeek/index.tsx b/apps/scandic-web/components/Blocks/DynamicContent/Stays/Upcoming/SidePeek/index.tsx new file mode 100644 index 000000000..dd8aafcc2 --- /dev/null +++ b/apps/scandic-web/components/Blocks/DynamicContent/Stays/Upcoming/SidePeek/index.tsx @@ -0,0 +1,118 @@ +"use client" + +import { useIntl } from "react-intl" + +import { useSidePeekScrollToTop } from "@scandic-hotels/common/hooks/useSidePeekScrollToTop" +import { BackToTopButton } from "@scandic-hotels/design-system/BackToTopButton" +import { LoadingSpinner } from "@scandic-hotels/design-system/LoadingSpinner" +import SidePeekSelfControlled from "@scandic-hotels/design-system/SidePeekSelfControlled" +import { Typography } from "@scandic-hotels/design-system/Typography" +import { trpc } from "@scandic-hotels/trpc/client" + +import useLang from "@/hooks/useLang" + +import ShowMoreButton from "../../ShowMoreButton" +import { StayCard } from "../../StayCard" +import { groupStaysByYear } from "../../utils/groupStaysByYear" + +import styles from "./upcomingStaysSidePeek.module.css" + +import type { UpcomingStaysNonNullResponseObject } from "@/types/components/myPages/stays/upcoming" + +interface UpcomingStaysSidePeekProps { + isOpen: boolean + onClose: () => void +} + +export function UpcomingStaysSidePeek({ + isOpen, + onClose, +}: UpcomingStaysSidePeekProps) { + const intl = useIntl() + const lang = useLang() + + const { scrollContainerRef, showBackToTop, scrollToTop } = + useSidePeekScrollToTop() + + const { data, isFetching, fetchNextPage, hasNextPage, isLoading } = + trpc.user.stays.upcoming.useInfiniteQuery( + { + limit: 10, + lang, + includeFirstStay: true, + }, + { + getNextPageParam: (lastPage) => { + return lastPage?.nextCursor + }, + enabled: isOpen, + } + ) + + function loadMoreData() { + if (hasNextPage) { + fetchNextPage() + } + } + + const stays = data?.pages + .filter((page): page is UpcomingStaysNonNullResponseObject => !!page?.data) + .flatMap((page) => page.data) + + const staysByYear = stays ? groupStaysByYear(stays, "asc") : [] + + return ( + +
+ {isLoading ? ( +
+ +
+ ) : ( + <> + {staysByYear.map(({ year, stays }) => ( +
+
+ + {year} + +
+
+ {stays.map((stay) => ( + + ))} +
+
+ ))} + {hasNextPage && ( + + )} + + )} + {showBackToTop && !isLoading && ( + + )} +
+
+ ) +} diff --git a/apps/scandic-web/components/Blocks/DynamicContent/Stays/Upcoming/SidePeek/upcomingStaysSidePeek.module.css b/apps/scandic-web/components/Blocks/DynamicContent/Stays/Upcoming/SidePeek/upcomingStaysSidePeek.module.css new file mode 100644 index 000000000..aff4bc5ed --- /dev/null +++ b/apps/scandic-web/components/Blocks/DynamicContent/Stays/Upcoming/SidePeek/upcomingStaysSidePeek.module.css @@ -0,0 +1,35 @@ +.content { + display: flex; + flex-direction: column; + gap: var(--Space-x3); + position: relative; +} + +.loadingContainer { + display: flex; + justify-content: center; + align-items: center; + padding: var(--Space-x4); +} + +.yearSection { + display: flex; + flex-direction: column; + gap: var(--Space-x2); +} + +.yearHeader { + background: var(--Surface-Primary-Hover-Accent); + padding: var(--Space-x1) var(--Space-x2); + border-radius: var(--Corner-radius-sm); +} + +.yearText { + color: var(--Text-Heading); +} + +.staysList { + display: flex; + flex-direction: column; + gap: var(--Space-x2); +} diff --git a/apps/scandic-web/components/Blocks/DynamicContent/Stays/UpcomingStays/index.tsx b/apps/scandic-web/components/Blocks/DynamicContent/Stays/Upcoming/index.tsx similarity index 90% rename from apps/scandic-web/components/Blocks/DynamicContent/Stays/UpcomingStays/index.tsx rename to apps/scandic-web/components/Blocks/DynamicContent/Stays/Upcoming/index.tsx index 470240379..b696d81ca 100644 --- a/apps/scandic-web/components/Blocks/DynamicContent/Stays/UpcomingStays/index.tsx +++ b/apps/scandic-web/components/Blocks/DynamicContent/Stays/Upcoming/index.tsx @@ -5,9 +5,9 @@ import { Section } from "@/components/Section" import { SectionHeader } from "@/components/Section/Header" import SectionLink from "@/components/Section/Link" -import EmptyUpcomingStaysBlock from "../EmptyUpcomingStays" import UpcomingStaysCarousel from "./Carousel" -import ClientUpcomingStays from "./Client" +import EmptyUpcomingStays from "./EmptyUpcomingStays" +import ClientUpcomingStays from "./OldClient" import styles from "./upcoming.module.css" @@ -43,7 +43,7 @@ export default async function UpcomingStays({ {hasStays ? ( ) : ( - + )} diff --git a/apps/scandic-web/components/Blocks/DynamicContent/Stays/UpcomingStays/upcoming.module.css b/apps/scandic-web/components/Blocks/DynamicContent/Stays/Upcoming/upcoming.module.css similarity index 100% rename from apps/scandic-web/components/Blocks/DynamicContent/Stays/UpcomingStays/upcoming.module.css rename to apps/scandic-web/components/Blocks/DynamicContent/Stays/Upcoming/upcoming.module.css diff --git a/apps/scandic-web/components/Blocks/DynamicContent/Stays/UpcomingStays/Carousel.tsx b/apps/scandic-web/components/Blocks/DynamicContent/Stays/UpcomingStays/Carousel.tsx deleted file mode 100644 index 688bd59c8..000000000 --- a/apps/scandic-web/components/Blocks/DynamicContent/Stays/UpcomingStays/Carousel.tsx +++ /dev/null @@ -1,67 +0,0 @@ -"use client" - -import { LoadingSpinner } from "@scandic-hotels/design-system/LoadingSpinner" -import { trpc } from "@scandic-hotels/trpc/client" - -import { Carousel } from "@/components/Carousel" -import useLang from "@/hooks/useLang" - -import CarouselCard from "./CarouselCard" - -import styles from "./upcoming.module.css" - -import type { - UpcomingStaysClientProps, - UpcomingStaysNonNullResponseObject, -} from "@/types/components/myPages/stays/upcoming" - -export default function UpcomingStaysCarousel({ - initialUpcomingStays, -}: UpcomingStaysClientProps) { - const lang = useLang() - const { data, isLoading } = trpc.user.stays.upcoming.useInfiniteQuery( - { - limit: 6, - lang, - }, - { - getNextPageParam: (lastPage) => { - return lastPage?.nextCursor - }, - initialData: { - pageParams: [undefined, 1], - pages: [initialUpcomingStays], - }, - } - ) - - if (isLoading) { - return - } - - const stays = data.pages - .filter((page): page is UpcomingStaysNonNullResponseObject => !!page?.data) - .flatMap((page) => page.data) - - if (!stays.length) { - return null - } - - return ( - - - {stays.map((stay) => ( - - - - ))} - - - - - - ) -} diff --git a/apps/scandic-web/components/Blocks/DynamicContent/Stays/Previous/utils/groupStaysByYear.ts b/apps/scandic-web/components/Blocks/DynamicContent/Stays/utils/groupStaysByYear.ts similarity index 56% rename from apps/scandic-web/components/Blocks/DynamicContent/Stays/Previous/utils/groupStaysByYear.ts rename to apps/scandic-web/components/Blocks/DynamicContent/Stays/utils/groupStaysByYear.ts index 9c5107f85..f0c1cb1fc 100644 --- a/apps/scandic-web/components/Blocks/DynamicContent/Stays/Previous/utils/groupStaysByYear.ts +++ b/apps/scandic-web/components/Blocks/DynamicContent/Stays/utils/groupStaysByYear.ts @@ -7,11 +7,18 @@ export interface StaysByYear { stays: Stay[] } +type SortOrder = "asc" | "desc" + /** * Groups stays by year based on checkinDate. - * @returns an array sorted by year in descending order (most recent first). + * @param stays - Array of stays to group + * @param sortOrder - Sort order for years: "desc" (most recent first) or "asc" (earliest first). Defaults to "desc". + * @returns an array sorted by year in the specified order. */ -export function groupStaysByYear(stays: Stay[]): StaysByYear[] { +export function groupStaysByYear( + stays: Stay[], + sortOrder: SortOrder = "desc" +): StaysByYear[] { const groupedMap = new Map() for (const stay of stays) { @@ -24,6 +31,8 @@ export function groupStaysByYear(stays: Stay[]): StaysByYear[] { } return Array.from(groupedMap.entries()) - .sort(([yearA], [yearB]) => yearB - yearA) + .sort(([yearA], [yearB]) => + sortOrder === "asc" ? yearA - yearB : yearB - yearA + ) .map(([year, stays]) => ({ year, stays })) } diff --git a/apps/scandic-web/components/Blocks/DynamicContent/index.tsx b/apps/scandic-web/components/Blocks/DynamicContent/index.tsx index f49371c8f..d8a469e1f 100644 --- a/apps/scandic-web/components/Blocks/DynamicContent/index.tsx +++ b/apps/scandic-web/components/Blocks/DynamicContent/index.tsx @@ -21,7 +21,7 @@ import SASTierComparisonBlock from "@/components/Blocks/DynamicContent/SASTierCo import SignupFormWrapper from "@/components/Blocks/DynamicContent/SignupFormWrapper" import NextStay from "@/components/Blocks/DynamicContent/Stays/NextStay" import PreviousStays from "@/components/Blocks/DynamicContent/Stays/Previous" -import UpcomingStays from "@/components/Blocks/DynamicContent/Stays/UpcomingStays" +import UpcomingStays from "@/components/Blocks/DynamicContent/Stays/Upcoming" import { ProfilingConsentBanner } from "@/components/MyPages/ProfilingConsent/Banner" import { SJWidget } from "@/components/SJWidget" diff --git a/apps/scandic-web/utils/getRelativePastTime.test.ts b/apps/scandic-web/utils/getTimeAgoText.test.ts similarity index 74% rename from apps/scandic-web/utils/getRelativePastTime.test.ts rename to apps/scandic-web/utils/getTimeAgoText.test.ts index 41f99adce..34f897939 100644 --- a/apps/scandic-web/utils/getRelativePastTime.test.ts +++ b/apps/scandic-web/utils/getTimeAgoText.test.ts @@ -2,7 +2,7 @@ import { describe, expect, it } from "vitest" import { dt } from "@scandic-hotels/common/dt" -import { getRelativePastTime } from "./getRelativePastTime" +import { getTimeAgoText } from "./getTimeAgoText" import type { IntlShape, MessageDescriptor } from "react-intl" @@ -53,32 +53,32 @@ const mockIntl = { }, } as IntlShape -describe("getRelativePastTime", () => { +describe("getTimeAgoText", () => { describe("days ago (1-30 days)", () => { it("should return '1 day ago' for yesterday", () => { const yesterday = dt().subtract(1, "day").format("YYYY-MM-DD") - const result = getRelativePastTime(yesterday, mockIntl) + const result = getTimeAgoText(yesterday, mockIntl) expect(result).toBe("1 day ago") }) it("should return '2 days ago' for 2 days ago", () => { const twoDaysAgo = dt().subtract(2, "days").format("YYYY-MM-DD") - const result = getRelativePastTime(twoDaysAgo, mockIntl) + const result = getTimeAgoText(twoDaysAgo, mockIntl) expect(result).toBe("2 days ago") }) it("should return '15 days ago' for 15 days ago", () => { const fifteenDaysAgo = dt().subtract(15, "days").format("YYYY-MM-DD") - const result = getRelativePastTime(fifteenDaysAgo, mockIntl) + const result = getTimeAgoText(fifteenDaysAgo, mockIntl) expect(result).toBe("15 days ago") }) it("should return '30 days ago' for exactly 30 days ago (boundary)", () => { const thirtyDaysAgo = dt().subtract(30, "days").format("YYYY-MM-DD") - const result = getRelativePastTime(thirtyDaysAgo, mockIntl) + const result = getTimeAgoText(thirtyDaysAgo, mockIntl) expect(result).toBe("30 days ago") }) @@ -86,7 +86,7 @@ describe("getRelativePastTime", () => { it("should handle the full range from 1 to 30 days ago", () => { for (let days = 1; days <= 30; days++) { const pastDate = dt().subtract(days, "days").format("YYYY-MM-DD") - const result = getRelativePastTime(pastDate, mockIntl) + const result = getTimeAgoText(pastDate, mockIntl) if (days === 1) { expect(result).toBe("1 day ago") @@ -100,42 +100,42 @@ describe("getRelativePastTime", () => { describe("months ago (31-364 days)", () => { it("should return '1 month ago' for 31 days ago", () => { const thirtyOneDaysAgo = dt().subtract(31, "days").format("YYYY-MM-DD") - const result = getRelativePastTime(thirtyOneDaysAgo, mockIntl) + const result = getTimeAgoText(thirtyOneDaysAgo, mockIntl) expect(result).toBe("1 month ago") }) it("should return '1 month ago' for 45 days ago", () => { const fortyFiveDaysAgo = dt().subtract(45, "days").format("YYYY-MM-DD") - const result = getRelativePastTime(fortyFiveDaysAgo, mockIntl) + const result = getTimeAgoText(fortyFiveDaysAgo, mockIntl) expect(result).toBe("1 month ago") }) it("should return '2 months ago' for 60 days ago", () => { const sixtyDaysAgo = dt().subtract(60, "days").format("YYYY-MM-DD") - const result = getRelativePastTime(sixtyDaysAgo, mockIntl) + const result = getTimeAgoText(sixtyDaysAgo, mockIntl) expect(result).toBe("2 months ago") }) it("should return '6 months ago' for 180 days ago", () => { const sixMonthsAgo = dt().subtract(180, "days").format("YYYY-MM-DD") - const result = getRelativePastTime(sixMonthsAgo, mockIntl) + const result = getTimeAgoText(sixMonthsAgo, mockIntl) expect(result).toBe("6 months ago") }) it("should return '11 months ago' for 330 days ago", () => { const elevenMonthsAgo = dt().subtract(330, "days").format("YYYY-MM-DD") - const result = getRelativePastTime(elevenMonthsAgo, mockIntl) + const result = getTimeAgoText(elevenMonthsAgo, mockIntl) expect(result).toBe("11 months ago") }) it("should return '12 months ago' for 364 days ago (boundary)", () => { const twelveMonthsAgo = dt().subtract(364, "days").format("YYYY-MM-DD") - const result = getRelativePastTime(twelveMonthsAgo, mockIntl) + const result = getTimeAgoText(twelveMonthsAgo, mockIntl) expect(result).toBe("12 months ago") }) @@ -144,28 +144,28 @@ describe("getRelativePastTime", () => { describe("years ago (365+ days)", () => { it("should return '1 year ago' for exactly 365 days ago", () => { const oneYearAgo = dt().subtract(365, "days").format("YYYY-MM-DD") - const result = getRelativePastTime(oneYearAgo, mockIntl) + const result = getTimeAgoText(oneYearAgo, mockIntl) expect(result).toBe("1 year ago") }) it("should return '1 year ago' for 400 days ago", () => { const fourHundredDaysAgo = dt().subtract(400, "days").format("YYYY-MM-DD") - const result = getRelativePastTime(fourHundredDaysAgo, mockIntl) + const result = getTimeAgoText(fourHundredDaysAgo, mockIntl) expect(result).toBe("1 year ago") }) it("should return '2 years ago' for 730 days ago", () => { const twoYearsAgo = dt().subtract(730, "days").format("YYYY-MM-DD") - const result = getRelativePastTime(twoYearsAgo, mockIntl) + const result = getTimeAgoText(twoYearsAgo, mockIntl) expect(result).toBe("2 years ago") }) it("should return '5 years ago' for 5 years ago", () => { const fiveYearsAgo = dt().subtract(5, "years").format("YYYY-MM-DD") - const result = getRelativePastTime(fiveYearsAgo, mockIntl) + const result = getTimeAgoText(fiveYearsAgo, mockIntl) expect(result).toBe("5 years ago") }) @@ -184,8 +184,8 @@ describe("getRelativePastTime", () => { .startOf("day") .format("YYYY-MM-DD HH:mm") - const result1 = getRelativePastTime(dateWithTime1, mockIntl) - const result2 = getRelativePastTime(dateWithTime2, mockIntl) + const result1 = getTimeAgoText(dateWithTime1, mockIntl) + const result2 = getTimeAgoText(dateWithTime2, mockIntl) expect(result1).toBe("5 days ago") expect(result2).toBe("5 days ago") @@ -193,18 +193,24 @@ describe("getRelativePastTime", () => { it("should handle ISO date strings with timezone", () => { const isoDate = dt().subtract(7, "days").toISOString() - const result = getRelativePastTime(isoDate, mockIntl) + const result = getTimeAgoText(isoDate, mockIntl) expect(result).toBe("7 days ago") }) - it("should handle future dates (should return 0 days ago for same day)", () => { - // The function doesn't handle negative days specially - it just uses the diff calculation - // For future dates on the same day, diff will be 0 - const futureDate = dt().add(1, "day").startOf("day").format("YYYY-MM-DD") - const result = getRelativePastTime(futureDate, mockIntl) + it("should return empty string for future dates", () => { + // Add 2 days to ensure it's definitely in the future regardless of time of day + const futureDate = dt().add(2, "days").format("YYYY-MM-DD") + const result = getTimeAgoText(futureDate, mockIntl) - expect(result).toBe("0 days ago") + expect(result).toBe("") + }) + + it("should return empty string for dates far in the future", () => { + const farFutureDate = dt().add(1, "year").format("YYYY-MM-DD") + const result = getTimeAgoText(farFutureDate, mockIntl) + + expect(result).toBe("") }) }) @@ -213,24 +219,24 @@ describe("getRelativePastTime", () => { const date30 = dt().subtract(30, "days").format("YYYY-MM-DD") const date31 = dt().subtract(31, "days").format("YYYY-MM-DD") - expect(getRelativePastTime(date30, mockIntl)).toBe("30 days ago") - expect(getRelativePastTime(date31, mockIntl)).toBe("1 month ago") + expect(getTimeAgoText(date30, mockIntl)).toBe("30 days ago") + expect(getTimeAgoText(date31, mockIntl)).toBe("1 month ago") }) it("should transition correctly from months to years at 365 days", () => { const date364 = dt().subtract(364, "days").format("YYYY-MM-DD") const date365 = dt().subtract(365, "days").format("YYYY-MM-DD") - expect(getRelativePastTime(date364, mockIntl)).toBe("12 months ago") - expect(getRelativePastTime(date365, mockIntl)).toBe("1 year ago") + expect(getTimeAgoText(date364, mockIntl)).toBe("12 months ago") + expect(getTimeAgoText(date365, mockIntl)).toBe("1 year ago") }) it("should handle the transition from 1 day to multiple days", () => { const date1 = dt().subtract(1, "day").format("YYYY-MM-DD") const date2 = dt().subtract(2, "days").format("YYYY-MM-DD") - expect(getRelativePastTime(date1, mockIntl)).toBe("1 day ago") - expect(getRelativePastTime(date2, mockIntl)).toBe("2 days ago") + expect(getTimeAgoText(date1, mockIntl)).toBe("1 day ago") + expect(getTimeAgoText(date2, mockIntl)).toBe("2 days ago") }) }) }) diff --git a/apps/scandic-web/utils/getRelativePastTime.ts b/apps/scandic-web/utils/getTimeAgoText.ts similarity index 86% rename from apps/scandic-web/utils/getRelativePastTime.ts rename to apps/scandic-web/utils/getTimeAgoText.ts index edffc9129..8985d4322 100644 --- a/apps/scandic-web/utils/getRelativePastTime.ts +++ b/apps/scandic-web/utils/getTimeAgoText.ts @@ -9,15 +9,19 @@ import type { IntlShape } from "react-intl" * - 1-30 days: "1 day ago", "15 days ago", "30 days ago" * - 31-364 days: "1 month ago", "6 months ago", "12 months ago" * - 365+ days: "1 year ago", "2 years ago", "5 years ago" + * - Returns empty string for future dates. + * */ -export function getRelativePastTime( - checkoutDate: string, - intl: IntlShape -): string { +export function getTimeAgoText(checkoutDate: string, intl: IntlShape): string { const now = dt() const checkout = dt(checkoutDate) const daysDiff = now.diff(checkout, "days") + // Return empty string for future dates + if (daysDiff < 0) { + return "" + } + if (daysDiff <= 30) { // 1-30 days return intl.formatMessage( diff --git a/packages/trpc/lib/routers/user/input.ts b/packages/trpc/lib/routers/user/input.ts index 7a9f58106..0464f9b2e 100644 --- a/packages/trpc/lib/routers/user/input.ts +++ b/packages/trpc/lib/routers/user/input.ts @@ -14,6 +14,7 @@ export const staysInput = z .transform((num) => (num ? String(num) : undefined)), limit: z.number().min(0).default(6), lang: z.nativeEnum(Lang).optional(), + includeFirstStay: z.boolean().optional(), }) .default({}) diff --git a/packages/trpc/lib/routers/user/query/index.ts b/packages/trpc/lib/routers/user/query/index.ts index ac0ca06c8..860e58619 100644 --- a/packages/trpc/lib/routers/user/query/index.ts +++ b/packages/trpc/lib/routers/user/query/index.ts @@ -163,7 +163,7 @@ export const userQueryRouter = router({ upcoming: languageProtectedProcedure .input(staysInput) .query(async ({ ctx, input }) => { - const { limit, cursor, lang } = input + const { limit, cursor, lang, includeFirstStay } = input const language = lang || ctx.lang const data = await getUpcomingStays( @@ -186,6 +186,14 @@ export const userQueryRouter = router({ ) if (env.NEW_STAYS_ON_MY_PAGES) { + // When includeFirstStay is true (used by SidePeek), return all stays + if (includeFirstStay) { + return { + data: updatedData, + nextCursor, + } + } + if (updatedData.length <= 1) { // If there are 1 or fewer stays, return null since NextStay handles this return null