diff --git a/apps/scandic-web/.env.local.example b/apps/scandic-web/.env.local.example index d538bb7ef..ecc736506 100644 --- a/apps/scandic-web/.env.local.example +++ b/apps/scandic-web/.env.local.example @@ -66,3 +66,5 @@ DTMC_ENTRA_ID_ISSUER="" DTMC_ENTRA_ID_SECRET="" HOTEL_BRANDING="0" # 0 - disabled, 1 - enabled + +NEW_STAYS_ON_MY_PAGES="true" diff --git a/apps/scandic-web/components/Blocks/DynamicContent/Stays/EmptyUpcomingStays/emptyUpcomingStays.module.css b/apps/scandic-web/components/Blocks/DynamicContent/Stays/EmptyUpcomingStays/emptyUpcomingStays.module.css index 8f7937796..82a2e57f9 100644 --- a/apps/scandic-web/components/Blocks/DynamicContent/Stays/EmptyUpcomingStays/emptyUpcomingStays.module.css +++ b/apps/scandic-web/components/Blocks/DynamicContent/Stays/EmptyUpcomingStays/emptyUpcomingStays.module.css @@ -30,3 +30,20 @@ justify-content: center; align-items: center; } + +/* Styles for new empty upcoming stays design */ +.emptyUpcomingStaysContainer { + display: flex; + padding: var(--Space-x6); + flex-direction: column; + justify-content: center; + align-items: center; + gap: var(--Space-x3); + border-radius: var(--Corner-radius-lg); + background: var(--Surface-Brand-Primary-1-Default); +} + +.heading { + color: var(--Text-Heading); + text-align: center; +} diff --git a/apps/scandic-web/components/Blocks/DynamicContent/Stays/EmptyUpcomingStays/index.tsx b/apps/scandic-web/components/Blocks/DynamicContent/Stays/EmptyUpcomingStays/index.tsx index fde2d930a..e71f1f248 100644 --- a/apps/scandic-web/components/Blocks/DynamicContent/Stays/EmptyUpcomingStays/index.tsx +++ b/apps/scandic-web/components/Blocks/DynamicContent/Stays/EmptyUpcomingStays/index.tsx @@ -1,6 +1,10 @@ +import ButtonLink from "@scandic-hotels/design-system/ButtonLink" import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon" import Link from "@scandic-hotels/design-system/OldDSLink" import Title from "@scandic-hotels/design-system/Title" +import { Typography } from "@scandic-hotels/design-system/Typography" + +import { env } from "@/env/server" import { getIntl } from "@/i18n" import { getLang } from "@/i18n/serverContext" @@ -13,39 +17,60 @@ export default async function EmptyUpcomingStaysBlock() { const href = `/${lang}` - return ( -
-
- + <div className={styles.titleContainer}> + <Title + as="h4" + level="h3" + color="red" + className={styles.title} + textAlign="center" + > + {intl.formatMessage({ + id: "stays.noUpcomingStays", + defaultMessage: "You have no upcoming stays.", + })} + <span className={styles.burgundyTitle}> + {intl.formatMessage({ + id: "stays.whereToGoNext", + defaultMessage: "Where should you go next?", + })} + </span> + +
+ {intl.formatMessage({ - id: "stays.noUpcomingStays", - defaultMessage: "You have no upcoming stays.", + id: "stays.getInspired", + defaultMessage: "Get inspired", })} - - {intl.formatMessage({ - id: "stays.whereToGoNext", - defaultMessage: "Where should you go next?", - })} - - - - + + +
+ ) + } + + return ( +
+ +

+ {intl.formatMessage({ + id: "stays.noUpcomingStaysAtTheMoment", + defaultMessage: "You have no upcoming stays at the moment", + })} +

+
+ {intl.formatMessage({ - id: "stays.getInspired", - defaultMessage: "Get inspired", + id: "stays.findDestinationOrHotel", + defaultMessage: "Find destination or hotel", })} - - +
) } 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 6385a9c8b..91ee95bc6 100644 --- a/apps/scandic-web/components/Blocks/DynamicContent/Stays/NextStay/NextStayContent.tsx +++ b/apps/scandic-web/components/Blocks/DynamicContent/Stays/NextStay/NextStayContent.tsx @@ -8,7 +8,7 @@ import { Typography } from "@scandic-hotels/design-system/Typography" import { getIntl } from "@/i18n" import { getLang } from "@/i18n/serverContext" -import { getDaysUntilText } from "./utils" +import { getDaysUntilText } from "../utils/getDaysUntilText" import styles from "./nextStay.module.css" 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 43a163e91..3ae28723d 100644 --- a/apps/scandic-web/components/Blocks/DynamicContent/Stays/NextStay/index.tsx +++ b/apps/scandic-web/components/Blocks/DynamicContent/Stays/NextStay/index.tsx @@ -1,9 +1,11 @@ +import { env } from "@/env/server" import { serverClient } from "@/lib/trpc/server" import { Section } from "@/components/Section" import { SectionHeader } from "@/components/Section/Header" import SectionLink from "@/components/Section/Link" +import EmptyUpcomingStaysBlock from "../EmptyUpcomingStays" import NextStayContent from "./NextStayContent" import styles from "./nextStay.module.css" @@ -15,7 +17,7 @@ export default async function NextStay({ title, link }: NextStayProps) { const nextStay = await caller.user.stays.next() if (!nextStay) { - return 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 45315ad90..c545f9b09 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 @@ -29,10 +29,7 @@ .imageOverlay { position: absolute; - top: 0; - left: 0; - right: 0; - bottom: 0; + inset: 0; background: linear-gradient( to bottom, rgba(0, 0, 0, 0.1) 0%, diff --git a/apps/scandic-web/components/Blocks/DynamicContent/Stays/Upcoming/upcoming.module.css b/apps/scandic-web/components/Blocks/DynamicContent/Stays/Upcoming/upcoming.module.css deleted file mode 100644 index 5abd71abb..000000000 --- a/apps/scandic-web/components/Blocks/DynamicContent/Stays/Upcoming/upcoming.module.css +++ /dev/null @@ -1,3 +0,0 @@ -.container { - display: inline-grid; -} diff --git a/apps/scandic-web/components/Blocks/DynamicContent/Stays/UpcomingStays/Carousel.tsx b/apps/scandic-web/components/Blocks/DynamicContent/Stays/UpcomingStays/Carousel.tsx new file mode 100644 index 000000000..688bd59c8 --- /dev/null +++ b/apps/scandic-web/components/Blocks/DynamicContent/Stays/UpcomingStays/Carousel.tsx @@ -0,0 +1,67 @@ +"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/UpcomingStays/CarouselCard/carouselCard.module.css b/apps/scandic-web/components/Blocks/DynamicContent/Stays/UpcomingStays/CarouselCard/carouselCard.module.css new file mode 100644 index 000000000..8cf5b3f4b --- /dev/null +++ b/apps/scandic-web/components/Blocks/DynamicContent/Stays/UpcomingStays/CarouselCard/carouselCard.module.css @@ -0,0 +1,69 @@ +.card { + display: flex; + flex-direction: column; + background: var(--Base-Surface-Primary-light-Normal); + border: 1px solid var(--Border-Default); + overflow: hidden; + border-radius: var(--Corner-radius-lg); +} + +.imageContainer { + position: relative; + width: 100%; + aspect-ratio: 16/9; + border-radius: var(--Corner-radius-lg) var(--Corner-radius-lg) 0 0; + background: + linear-gradient(0deg, rgba(0, 0, 0, 0.4) 0%, rgba(0, 0, 0, 0.4) 100%), + lightgray 50% / cover no-repeat, + var(--Neutral-20); + overflow: hidden; +} + +.image { + width: 100%; + height: 100%; + object-fit: cover; + position: relative; + z-index: 1; +} + +.imageOverlay { + position: absolute; + inset: 0; + background: linear-gradient( + 180deg, + rgba(0, 0, 0, 0.4) 0%, + rgba(0, 0, 0, 0.2) 50%, + rgba(0, 0, 0, 0.6) 100% + ); + display: flex; + flex-direction: column; + z-index: 2; + padding: var(--Space-x2); + color: var(--Text-Inverted); + place-content: center; + text-align: center; +} + +.content { + display: flex; + flex-direction: column; + gap: var(--Space-x1); + padding: var(--Space-x2); +} + +.infoRow { + display: flex; + justify-content: space-between; + align-items: center; +} + +.infoItem { + display: flex; + align-items: center; + gap: var(--Space-x05); +} + +.dateRange { + text-align: right; +} diff --git a/apps/scandic-web/components/Blocks/DynamicContent/Stays/UpcomingStays/CarouselCard/index.tsx b/apps/scandic-web/components/Blocks/DynamicContent/Stays/UpcomingStays/CarouselCard/index.tsx new file mode 100644 index 000000000..98fe6fd8f --- /dev/null +++ b/apps/scandic-web/components/Blocks/DynamicContent/Stays/UpcomingStays/CarouselCard/index.tsx @@ -0,0 +1,114 @@ +"use client" + +import { useIntl } from "react-intl" + +import { dt } from "@scandic-hotels/common/dt" +import ButtonLink from "@scandic-hotels/design-system/ButtonLink" +import { Divider } from "@scandic-hotels/design-system/Divider" +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 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" + +interface CarouselCardProps { + stay: Stay +} + +export default function CarouselCard({ stay }: CarouselCardProps) { + const intl = useIntl() + const lang = useLang() + + const { attributes } = stay + const { + checkinDate, + checkoutDate, + hotelInformation, + isWebAppOrigin, + bookingUrl, + } = attributes + + const daysUntilText = getDaysUntilText(checkinDate, lang, intl) + + return ( +
+
+ { +
+ + {daysUntilText} + + + {hotelInformation.hotelName} + + {hotelInformation.cityName && ( + + {hotelInformation.cityName} + + )} +
+
+ +
+
+ + + + {intl.formatMessage({ + id: "common.dates", + defaultMessage: "Dates", + })} + + + + + + {/* eslint-disable-next-line formatjs/no-literal-string-in-jsx */} + {" → "} + + + +
+ + {isWebAppOrigin && ( + <> + + + {intl.formatMessage({ + id: "nextStay.seeDetailsAndExtras", + defaultMessage: "See details & extras", + })} + + + + )} +
+
+ ) +} diff --git a/apps/scandic-web/components/Blocks/DynamicContent/Stays/Upcoming/Client.tsx b/apps/scandic-web/components/Blocks/DynamicContent/Stays/UpcomingStays/Client.tsx similarity index 100% rename from apps/scandic-web/components/Blocks/DynamicContent/Stays/Upcoming/Client.tsx rename to apps/scandic-web/components/Blocks/DynamicContent/Stays/UpcomingStays/Client.tsx diff --git a/apps/scandic-web/components/Blocks/DynamicContent/Stays/Upcoming/index.tsx b/apps/scandic-web/components/Blocks/DynamicContent/Stays/UpcomingStays/index.tsx similarity index 56% rename from apps/scandic-web/components/Blocks/DynamicContent/Stays/Upcoming/index.tsx rename to apps/scandic-web/components/Blocks/DynamicContent/Stays/UpcomingStays/index.tsx index d96e04f78..470240379 100644 --- a/apps/scandic-web/components/Blocks/DynamicContent/Stays/Upcoming/index.tsx +++ b/apps/scandic-web/components/Blocks/DynamicContent/Stays/UpcomingStays/index.tsx @@ -1,10 +1,12 @@ +import { env } from "@/env/server" import { serverClient } from "@/lib/trpc/server" import { Section } from "@/components/Section" -import SectionHeader from "@/components/Section/Header/Deprecated" +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 styles from "./upcoming.module.css" @@ -20,10 +22,25 @@ export default async function UpcomingStays({ limit: 6, }) + const hasStays = + initialUpcomingStays?.data && initialUpcomingStays.data.length > 0 + + if (env.NEW_STAYS_ON_MY_PAGES) { + if (!hasStays) return null + + return ( +
+ {title && } + + +
+ ) + } + return (
- - {initialUpcomingStays?.data.length ? ( + {title && } + {hasStays ? ( ) : ( diff --git a/apps/scandic-web/components/Blocks/DynamicContent/Stays/UpcomingStays/upcoming.module.css b/apps/scandic-web/components/Blocks/DynamicContent/Stays/UpcomingStays/upcoming.module.css new file mode 100644 index 000000000..6d945dafa --- /dev/null +++ b/apps/scandic-web/components/Blocks/DynamicContent/Stays/UpcomingStays/upcoming.module.css @@ -0,0 +1,11 @@ +.container { + display: inline-grid; +} + +.carousel { + width: 100%; +} + +.carousel .navigationButton { + top: 40%; +} diff --git a/apps/scandic-web/components/Blocks/DynamicContent/Stays/NextStay/utils.test.ts b/apps/scandic-web/components/Blocks/DynamicContent/Stays/utils/getDaysUntilText.test.ts similarity index 99% rename from apps/scandic-web/components/Blocks/DynamicContent/Stays/NextStay/utils.test.ts rename to apps/scandic-web/components/Blocks/DynamicContent/Stays/utils/getDaysUntilText.test.ts index e7cd5eb11..9d474cc25 100644 --- a/apps/scandic-web/components/Blocks/DynamicContent/Stays/NextStay/utils.test.ts +++ b/apps/scandic-web/components/Blocks/DynamicContent/Stays/utils/getDaysUntilText.test.ts @@ -3,7 +3,7 @@ import { describe, expect, it } from "vitest" import { Lang } from "@scandic-hotels/common/constants/language" import { dt } from "@scandic-hotels/common/dt" -import { getDaysUntilText } from "./utils" +import { getDaysUntilText } from "./getDaysUntilText" import type { IntlShape, MessageDescriptor } from "react-intl" @@ -13,7 +13,6 @@ const mockIntl = { values?: Record ) => { const messages: Record = { - "nextStay.past": `{date}`, "nextStay.today": "Today", "nextStay.tomorrow": "Tomorrow", "nextStay.inXDays": `In {days} days`, diff --git a/apps/scandic-web/components/Blocks/DynamicContent/Stays/NextStay/utils.ts b/apps/scandic-web/components/Blocks/DynamicContent/Stays/utils/getDaysUntilText.ts similarity index 86% rename from apps/scandic-web/components/Blocks/DynamicContent/Stays/NextStay/utils.ts rename to apps/scandic-web/components/Blocks/DynamicContent/Stays/utils/getDaysUntilText.ts index c352d5f93..2ee7e4a64 100644 --- a/apps/scandic-web/components/Blocks/DynamicContent/Stays/NextStay/utils.ts +++ b/apps/scandic-web/components/Blocks/DynamicContent/Stays/utils/getDaysUntilText.ts @@ -14,15 +14,7 @@ export function getDaysUntilText( // Handle past dates edge case. if (daysUntil < 0) { - return intl.formatMessage( - { - id: "nextStay.past", - defaultMessage: "{date} ", - }, - { - date: dt(checkinDate).locale(lang).format("D MMM YYYY"), - } - ) + return dt(checkinDate).locale(lang).format("D MMM YYYY") } if (daysUntil === 0) { diff --git a/apps/scandic-web/components/Blocks/DynamicContent/index.tsx b/apps/scandic-web/components/Blocks/DynamicContent/index.tsx index c70ebf0c7..3f8ae1448 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/Upcoming" +import UpcomingStays from "@/components/Blocks/DynamicContent/Stays/UpcomingStays" import { SJWidget } from "@/components/SJWidget" import JobylonFeed from "./JobylonFeed" diff --git a/apps/scandic-web/env/server.ts b/apps/scandic-web/env/server.ts index c95c1520d..824e707c1 100644 --- a/apps/scandic-web/env/server.ts +++ b/apps/scandic-web/env/server.ts @@ -136,6 +136,13 @@ export const env = createEnv({ .string() .optional() .transform((s) => s?.split(",") || []), + NEW_STAYS_ON_MY_PAGES: z + .string() + // only allow "true" or "false" + .refine((s) => s === "true" || s === "false") + // transform to boolean + .transform((s) => s === "true") + .default("false"), }, emptyStringAsUndefined: true, runtimeEnv: { @@ -213,6 +220,7 @@ export const env = createEnv({ DTMC_ENTRA_ID_SECRET: process.env.DTMC_ENTRA_ID_SECRET, HOTEL_BRANDING: process.env.HOTEL_BRANDING, CHATBOT_LIVE_LANGS: process.env.CHATBOT_LIVE_LANGS, + NEW_STAYS_ON_MY_PAGES: process.env.NEW_STAYS_ON_MY_PAGES, }, }) diff --git a/packages/booking-flow/lib/components/SelectRate/RoomsContainer/Rooms/RoomsHeader/roomsHeader.module.css b/packages/booking-flow/lib/components/SelectRate/RoomsContainer/Rooms/RoomsHeader/roomsHeader.module.css index ac15aa4dd..33818d6a2 100644 --- a/packages/booking-flow/lib/components/SelectRate/RoomsContainer/Rooms/RoomsHeader/roomsHeader.module.css +++ b/packages/booking-flow/lib/components/SelectRate/RoomsContainer/Rooms/RoomsHeader/roomsHeader.module.css @@ -2,7 +2,7 @@ display: grid; gap: var(--Space-x3); align-items: center; - grid-template-areas: + grid-template-areas: "availableRoomsCount" "noAvailabilityAlert" "filters"; @@ -26,7 +26,8 @@ @media screen and (min-width: 768px) { .container { grid-template-columns: 1fr auto; - grid-template-areas: "availableRoomsCount filters" - "noAvailabilityAlert noAvailabilityAlert"; + grid-template-areas: + "availableRoomsCount filters" + "noAvailabilityAlert noAvailabilityAlert"; } } diff --git a/packages/design-system/lib/fonts.css b/packages/design-system/lib/fonts.css index e3429d0d5..5b373c1b6 100644 --- a/packages/design-system/lib/fonts.css +++ b/packages/design-system/lib/fonts.css @@ -276,7 +276,7 @@ font-style: normal; font-weight: 400; font-display: block; - src: url(/_static/shared/fonts/material-symbols/rounded-1db5531f.woff2) + src: url(/_static/shared/fonts/material-symbols/rounded-1b8067b7.woff2) format('woff2'); } diff --git a/packages/trpc/env/server.ts b/packages/trpc/env/server.ts index 4cd42a0d5..e2567bf77 100644 --- a/packages/trpc/env/server.ts +++ b/packages/trpc/env/server.ts @@ -36,6 +36,13 @@ export const env = createEnv({ SENTRY_ENVIRONMENT: z.string().default("development"), PUBLIC_URL: z.string().default(""), SALESFORCE_PREFERENCE_BASE_URL: z.string(), + NEW_STAYS_ON_MY_PAGES: z + .string() + // only allow "true" or "false" + .refine((s) => s === "true" || s === "false") + // transform to boolean + .transform((s) => s === "true") + .default("false"), }, emptyStringAsUndefined: true, runtimeEnv: { @@ -56,5 +63,6 @@ export const env = createEnv({ SENTRY_ENVIRONMENT: process.env.SENTRY_ENVIRONMENT, PUBLIC_URL: process.env.NEXT_PUBLIC_PUBLIC_URL, SALESFORCE_PREFERENCE_BASE_URL: process.env.SALESFORCE_PREFERENCE_BASE_URL, + NEW_STAYS_ON_MY_PAGES: process.env.NEW_STAYS_ON_MY_PAGES, }, }) diff --git a/packages/trpc/lib/routers/user/query/index.ts b/packages/trpc/lib/routers/user/query/index.ts index 967b50d89..1191fddfd 100644 --- a/packages/trpc/lib/routers/user/query/index.ts +++ b/packages/trpc/lib/routers/user/query/index.ts @@ -1,6 +1,7 @@ import { createCounter } from "@scandic-hotels/common/telemetry" import { safeTry } from "@scandic-hotels/common/utils/safeTry" +import { env } from "../../../../env/server" import { router } from "../../.." import * as api from "../../../api" import { Transactions } from "../../../enums/transactions" @@ -184,6 +185,20 @@ export const userQueryRouter = router({ language ) + if (env.NEW_STAYS_ON_MY_PAGES) { + if (updatedData.length <= 1) { + // If there are 1 or fewer stays, return null since NextStay handles this + return null + } + + // If there are multiple stays, filter out the first one since NextStay shows it + const filteredData = updatedData.slice(1) + return { + data: filteredData, + nextCursor, + } + } + return { data: updatedData, nextCursor, diff --git a/scripts/material-symbols-update.mts b/scripts/material-symbols-update.mts index 2d78c87d6..dde9a2047 100644 --- a/scripts/material-symbols-update.mts +++ b/scripts/material-symbols-update.mts @@ -19,327 +19,329 @@ const FONT_BASE_URL = `https://fonts.googleapis.com/css2?family=Material+Symbols // Defines the subset of icons for the font const icons = [ - "accessibility", - "accessible", - "add_circle", - "add", - "air_purifier_gen", - "air", - "airline_seat_recline_normal", - "airplane_ticket", - "apartment", - "apparel", - "arrow_back_ios", - "arrow_back", - "arrow_forward_ios", - "arrow_forward", - "arrow_right", - "arrow_upward", - "assistant_navigation", - "asterisk", - "attractions", - "award_star", - "bakery_dining", - "balcony", - "bathroom", - "bathtub", - "beach_access", - "bed", - "bedroom_parent", - "box", - "brunch_dining", - "business_center", - "calendar_add_on", - "calendar_clock", - "calendar_month", - "calendar_today", - "call_quality", - "call", - "camera", - "cancel", - "chair", - "charging_station", - "check_box", - "check_circle", - "check", - "checked_bag", - "checkroom", - "chevron_left", - "chevron_right", - "close", - "coffee_maker", - "coffee", - "compare_arrows", - "computer", - "concierge", - "confirmation_number", - "connected_tv", - "content_copy", - "contract", - "cool_to_dry", - "countertops", - "credit_card_heart", - "credit_card", - "curtains_closed", - "curtains", - "deck", - "delete", - "desk", - "device_thermostat", - "diamond", - "dining", - "directions_run", - "directions_subway", - "directions", - "downhill_skiing", - "download", - "dresser", - "edit_calendar", - "edit_square", - "edit", - "electric_bike", - "electric_car", - "elevator", - "emoji_transportation", - "error_circle_rounded", - "error", - "exercise", - "family_restroom", - "fastfood", - "favorite", - "fax", - "featured_seasonal_and_gifts", - "festival", - "filter_alt", - "filter", - "format_list_bulleted", - "floor_lamp", - "forest", - "garage", - "globe", - "golf_course", - "groups", - "health_and_beauty", - "heat", - "hiking", - "home", - "hot_tub", - "houseboat", - "hvac", - "id_card", - "imagesmode", - "info", - "iron", - "kayaking", - "kettle", - "keyboard_arrow_down", - "keyboard_arrow_up", - "king_bed", - "kitchen", - "landscape", - "laundry", - "link", - "liquor", - "live_tv", - "local_bar", - "local_cafe", - "local_convenience_store", - "local_drink", - "local_laundry_service", - "local_parking", - "location_city", - "location_on", - "lock", - "loyalty", - "luggage", - "mail", - "map", - "meeting_room", - "microwave", - "mode_fan", - "museum", - "music_cast", - "music_note", - "nature", - "night_shelter", - "nightlife", - "open_in_new", - "pan_zoom", - "panorama", - "pedal_bike", - "person", - "pets", - "phone", - "photo_camera", - "pool", - "print", - "radio", - "recommend", - "redeem", - "refresh", - "remove", - "restaurant", - "room_service", - "router", - "sailing", - "sauna", - "scene", - "search", - "sell", - "shopping_bag", - "shower", - "single_bed", - "skateboarding", - "smoke_free", - "smoking_rooms", - "spa", - "sports_esports", - "sports_golf", - "sports_handball", - "sports_tennis", - "stairs", - "star", - "straighten", - "styler", - "support_agent", - "swipe", - "sync_saved_locally", - "table_bar", - "theater_comedy", - "things_to_do", - "train", - "tram", - "transit_ticket", - "travel_luggage_and_bags", - "travel", - "trophy", - "tv_guide", - "tv_remote", - "upload", - "visibility_off", - "visibility", - "ward", - "warning", - "water_full", - "wifi", - "yard", + "accessibility", + "accessible", + "add_circle", + "add", + "air_purifier_gen", + "air", + "airline_seat_recline_normal", + "airplane_ticket", + "apartment", + "apparel", + "arrow_back_ios", + "arrow_back", + "arrow_forward_ios", + "arrow_forward", + "arrow_right", + "arrow_upward", + "assistant_navigation", + "asterisk", + "attractions", + "award_star", + "bakery_dining", + "balcony", + "bathroom", + "bathtub", + "beach_access", + "bed", + "bedroom_parent", + "box", + "brunch_dining", + "business_center", + "calendar_add_on", + "calendar_clock", + "calendar_month", + "calendar_today", + "call_quality", + "call", + "camera", + "cancel", + "chair", + "charging_station", + "check_box", + "check_circle", + "check", + "checked_bag", + "checkroom", + "chevron_left", + "chevron_right", + "close", + "coffee_maker", + "coffee", + "compare_arrows", + "computer", + "concierge", + "confirmation_number", + "connected_tv", + "content_copy", + "contract", + "cool_to_dry", + "countertops", + "credit_card_heart", + "credit_card", + "curtains_closed", + "curtains", + "deck", + "delete", + "desk", + "device_thermostat", + "diamond", + "dining", + "directions_run", + "directions_subway", + "directions", + "downhill_skiing", + "download", + "dresser", + "edit_calendar", + "edit_square", + "edit", + "electric_bike", + "electric_car", + "elevator", + "emoji_transportation", + "error_circle_rounded", + "error", + "exercise", + "family_restroom", + "fastfood", + "favorite", + "fax", + "featured_seasonal_and_gifts", + "festival", + "filter_alt", + "filter", + "format_list_bulleted", + "floor_lamp", + "forest", + "garage", + "globe", + "golf_course", + "groups", + "health_and_beauty", + "heat", + "hiking", + "home", + "hot_tub", + "houseboat", + "hvac", + "id_card", + "imagesmode", + "info", + "iron", + "kayaking", + "kettle", + "keyboard_arrow_down", + "keyboard_arrow_right", + "keyboard_arrow_up", + "king_bed", + "kitchen", + "landscape", + "laundry", + "link", + "liquor", + "live_tv", + "local_bar", + "local_cafe", + "local_convenience_store", + "local_drink", + "local_laundry_service", + "local_parking", + "location_city", + "location_on", + "lock", + "loyalty", + "luggage", + "mail", + "map", + "meeting_room", + "microwave", + "mode_fan", + "museum", + "music_cast", + "music_note", + "nature", + "night_shelter", + "nightlife", + "open_in_new", + "pan_zoom", + "panorama", + "pedal_bike", + "person", + "pets", + "phone", + "photo_camera", + "pool", + "print", + "radio", + "recommend", + "redeem", + "refresh", + "remove", + "restaurant", + "room_service", + "router", + "sailing", + "sauna", + "scene", + "search", + "sell", + "shopping_bag", + "shower", + "single_bed", + "skateboarding", + "smoke_free", + "smoking_rooms", + "spa", + "sports_esports", + "sports_golf", + "sports_handball", + "sports_tennis", + "stairs", + "star", + "straighten", + "styler", + "support_agent", + "swipe", + "sync_saved_locally", + "table_bar", + "theater_comedy", + "things_to_do", + "train", + "tram", + "transit_ticket", + "travel_luggage_and_bags", + "travel", + "trophy", + "tv_guide", + "tv_remote", + "upload", + "visibility_off", + "visibility", + "ward", + "warning", + "water_full", + "wifi", + "yard", ].sort(); function createHash(value: unknown) { - const stringified = stringify(value); - const hash = crypto.createHash("sha256"); - hash.update(stringified); - return hash.digest("hex"); + const stringified = stringify(value); + const hash = crypto.createHash("sha256"); + hash.update(stringified); + return hash.digest("hex"); } const hash = createHash(icons).substring(0, 8); async function fetchIconUrl(url: string) { - const response = await fetch(url, { - headers: { - Accept: "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", - "User-Agent": - "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36", - }, - }); + const response = await fetch(url, { + headers: { + Accept: + "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", + "User-Agent": + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36", + }, + }); - if (!response.ok) { - console.error(`Unable to fetch woff2 for ${url}`); - process.exit(1); - } + if (!response.ok) { + console.error(`Unable to fetch woff2 for ${url}`); + process.exit(1); + } - const text = await response.text(); + const text = await response.text(); - const isWoff2 = /format\('woff2'\)/.test(text); - if (!isWoff2) { - console.error(`Unable to identify woff2 font in response`); - process.exit(1); - } + const isWoff2 = /format\('woff2'\)/.test(text); + if (!isWoff2) { + console.error(`Unable to identify woff2 font in response`); + process.exit(1); + } - const srcUrl = text.match(/src: url\(([^)]+)\)/); + const srcUrl = text.match(/src: url\(([^)]+)\)/); - if (srcUrl && srcUrl[1]) { - return srcUrl[1]; - } + if (srcUrl && srcUrl[1]) { + return srcUrl[1]; + } - return null; + return null; } async function download(url: string, destFolder: string) { - const dest = resolve(join(destFolder, `/rounded-${hash}.woff2`)); + const dest = resolve(join(destFolder, `/rounded-${hash}.woff2`)); - try { - const response = await fetch(url); + try { + const response = await fetch(url); - if (!response.ok) { - console.error(`Unable to fetch ${url}`); - process.exit(1); - } - - if (!response.body) { - console.error(`Bad response from ${url}`); - process.exit(1); - } - - const fileStream = createWriteStream(dest); - - // @ts-expect-error: type mismatch - const readableNodeStream = Readable.fromWeb(response.body); - - await pipeline(readableNodeStream, fileStream); - } catch (error) { - console.error(`Error downloading file from ${url}:`, error); - process.exit(1); + if (!response.ok) { + console.error(`Unable to fetch ${url}`); + process.exit(1); } + + if (!response.body) { + console.error(`Bad response from ${url}`); + process.exit(1); + } + + const fileStream = createWriteStream(dest); + + // @ts-expect-error: type mismatch + const readableNodeStream = Readable.fromWeb(response.body); + + await pipeline(readableNodeStream, fileStream); + } catch (error) { + console.error(`Error downloading file from ${url}:`, error); + process.exit(1); + } } async function cleanFontDirs() { - await rm(FONT_DIR, { recursive: true, force: true }); - await mkdir(FONT_DIR, { recursive: true }); - await writeFile( - join(FONT_DIR, ".auto-generated"), - `Auto-generated, do not edit. Use scripts/material-symbols-update.mts to update.\nhash=${hash}\ncreated=${new Date().toISOString()}\n`, - { encoding: "utf-8" } - ); + await rm(FONT_DIR, { recursive: true, force: true }); + await mkdir(FONT_DIR, { recursive: true }); + await writeFile( + join(FONT_DIR, ".auto-generated"), + `Auto-generated, do not edit. Use scripts/material-symbols-update.mts to update.\nhash=${hash}\ncreated=${new Date().toISOString()}\n`, + { encoding: "utf-8" }, + ); } async function updateFontCSS() { - const file = resolve(__dirname, "../packages/design-system/lib/fonts.css"); + const file = resolve(__dirname, "../packages/design-system/lib/fonts.css"); - const css = await readFile(file, { - encoding: "utf-8", - }); + const css = await readFile(file, { + encoding: "utf-8", + }); - await writeFile( - file, - css.replace( - /url\(\/_static\/shared\/fonts\/material-symbols\/rounded[^)]+\)/, - `url(/_static/shared/fonts/material-symbols/rounded-${hash}.woff2)` - ), - { - encoding: "utf-8", - } - ); + await writeFile( + file, + css.replace( + /url\(\/_static\/shared\/fonts\/material-symbols\/rounded[^)]+\)/, + `url(/_static/shared/fonts/material-symbols/rounded-${hash}.woff2)`, + ), + { + encoding: "utf-8", + }, + ); } async function main() { - const fontUrl = `${FONT_BASE_URL}&icon_names=${icons.join(",")}&display=block`; + const fontUrl = `${FONT_BASE_URL}&icon_names=${icons.join(",")}&display=block`; - const iconUrl = await fetchIconUrl(fontUrl); + const iconUrl = await fetchIconUrl(fontUrl); - if (iconUrl) { - await cleanFontDirs(); + if (iconUrl) { + await cleanFontDirs(); - await download(iconUrl, FONT_DIR); + await download(iconUrl, FONT_DIR); - await updateFontCSS(); + await updateFontCSS(); - console.log("Successfully updated icons!"); - process.exit(0); - } else { - console.error( - `Unable to find the icon font src URL in CSS response from Google Fonts at ${fontUrl}` - ); - } + console.log("Successfully updated icons!"); + process.exit(0); + } else { + console.error( + `Unable to find the icon font src URL in CSS response from Google Fonts at ${fontUrl}`, + ); + } } main(); diff --git a/shared/fonts/material-symbols/.auto-generated b/shared/fonts/material-symbols/.auto-generated index 705247192..698e96b6c 100644 --- a/shared/fonts/material-symbols/.auto-generated +++ b/shared/fonts/material-symbols/.auto-generated @@ -1,3 +1,3 @@ Auto-generated, do not edit. Use scripts/material-symbols-update.mts to update. -hash=1db5531f -created=2025-09-17T06:58:37.841Z +hash=1b8067b7 +created=2025-11-11T10:02:22.385Z diff --git a/shared/fonts/material-symbols/rounded-1b8067b7.woff2 b/shared/fonts/material-symbols/rounded-1b8067b7.woff2 new file mode 100644 index 000000000..e593f745e Binary files /dev/null and b/shared/fonts/material-symbols/rounded-1b8067b7.woff2 differ diff --git a/shared/fonts/material-symbols/rounded-1db5531f.woff2 b/shared/fonts/material-symbols/rounded-1db5531f.woff2 deleted file mode 100644 index 381e1a340..000000000 Binary files a/shared/fonts/material-symbols/rounded-1db5531f.woff2 and /dev/null differ