diff --git a/apps/scandic-web/.env.local.example b/apps/scandic-web/.env.local.example index c1426f4e2..f16417451 100644 --- a/apps/scandic-web/.env.local.example +++ b/apps/scandic-web/.env.local.example @@ -54,6 +54,7 @@ GOOGLE_DYNAMIC_MAP_ID="" ENABLE_SURPRISES="true" ENABLE_DTMC="true" +ENABLE_NEW_OVERVIEW_SECTION="true" SHOW_SITE_WIDE_ALERT="false" diff --git a/apps/scandic-web/components/Blocks/DynamicContent/Overview/UserBaseInfo/index.tsx b/apps/scandic-web/components/Blocks/DynamicContent/Overview/UserBaseInfo/index.tsx new file mode 100644 index 000000000..1a1ae93cf --- /dev/null +++ b/apps/scandic-web/components/Blocks/DynamicContent/Overview/UserBaseInfo/index.tsx @@ -0,0 +1,58 @@ +import { Avatar } from "@scandic-hotels/design-system/Avatar" +import { Typography } from "@scandic-hotels/design-system/Typography" + +import CopyMembershipIdButton from "@/components/MyPages/CopyMembershipIdButton" +import { getIntl } from "@/i18n" +import { getInitials } from "@/utils/user" + +import styles from "./userBaseInfo.module.css" + +import type { User } from "@scandic-hotels/trpc/types/user" + +interface UserBaseInfoProps { + user: User +} + +export default async function UserBaseInfo({ user }: UserBaseInfoProps) { + const intl = await getIntl() + const initials = getInitials(user.firstName, user.lastName) + + return ( +
+ +
+ +

+ {user.firstName} {user.lastName} +

+
+
+ + + {intl.formatMessage({ + defaultMessage: "Membership ID:", + })} + + + {user.membership?.membershipNumber ? ( + + ) : ( + + + {intl.formatMessage({ + defaultMessage: "N/A", + })} + + + )} +
+
+
+ ) +} diff --git a/apps/scandic-web/components/Blocks/DynamicContent/Overview/UserBaseInfo/userBaseInfo.module.css b/apps/scandic-web/components/Blocks/DynamicContent/Overview/UserBaseInfo/userBaseInfo.module.css new file mode 100644 index 000000000..230ef4051 --- /dev/null +++ b/apps/scandic-web/components/Blocks/DynamicContent/Overview/UserBaseInfo/userBaseInfo.module.css @@ -0,0 +1,18 @@ +.container { + display: flex; + align-items: center; + gap: var(--Space-x15); +} + +.fullName { + color: var(--Text-Heading); + text-transform: capitalize; +} + +.membershipInfo { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: var(--Space-x05); + color: var(--Scandic-Red-100); +} diff --git a/apps/scandic-web/components/Blocks/DynamicContent/Overview/index.tsx b/apps/scandic-web/components/Blocks/DynamicContent/Overview/index.tsx index 631f5c6e8..18682829b 100644 --- a/apps/scandic-web/components/Blocks/DynamicContent/Overview/index.tsx +++ b/apps/scandic-web/components/Blocks/DynamicContent/Overview/index.tsx @@ -16,6 +16,7 @@ import Hero from "./Friend/Hero" import MembershipNumber from "./Friend/MembershipNumber" import Friend from "./Friend" import Stats from "./Stats" +import UserBaseInfo from "./UserBaseInfo" import styles from "./overview.module.css" @@ -57,6 +58,8 @@ export default async function Overview({ + {env.ENABLE_NEW_OVERVIEW_SECTION ? : null} + {/*TODO: Replace Hero Section Cards with New ones. */} diff --git a/apps/scandic-web/components/Header/MainMenu/Avatar/avatar.module.css b/apps/scandic-web/components/Header/MainMenu/Avatar/avatar.module.css deleted file mode 100644 index 577b720d2..000000000 --- a/apps/scandic-web/components/Header/MainMenu/Avatar/avatar.module.css +++ /dev/null @@ -1,14 +0,0 @@ -.avatar { - display: flex; - justify-content: center; - align-items: center; - overflow: hidden; - border-radius: var(--Corner-radius-rounded); - width: 2rem; - height: 2rem; - background-color: var(--UI-Grey-40); -} - -.initials { - background-color: var(--Base-Icon-Low-contrast); -} diff --git a/apps/scandic-web/components/Header/MainMenu/Avatar/index.tsx b/apps/scandic-web/components/Header/MainMenu/Avatar/index.tsx deleted file mode 100644 index db3cf3a98..000000000 --- a/apps/scandic-web/components/Header/MainMenu/Avatar/index.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import Footnote from "@scandic-hotels/design-system/Footnote" -import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon" -import Image from "@scandic-hotels/design-system/Image" - -import styles from "./avatar.module.css" - -import type { AvatarProps } from "@/types/components/header/avatar" - -export default function Avatar({ image, initials }: AvatarProps) { - let classNames = [styles.avatar] - let element = - if (image) { - classNames.push(styles.image) - element = {image.alt} - } else if (initials) { - classNames.push(styles.initials) - element = ( - - {initials} - - ) - } - return ( - - {element} - - ) -} diff --git a/apps/scandic-web/components/Header/MainMenu/MyPagesMenu/index.tsx b/apps/scandic-web/components/Header/MainMenu/MyPagesMenu/index.tsx index 49bf553c7..9537fe7c9 100644 --- a/apps/scandic-web/components/Header/MainMenu/MyPagesMenu/index.tsx +++ b/apps/scandic-web/components/Header/MainMenu/MyPagesMenu/index.tsx @@ -3,6 +3,7 @@ import { useRef } from "react" import { useIntl } from "react-intl" +import { Avatar } from "@scandic-hotels/design-system/Avatar" import Body from "@scandic-hotels/design-system/Body" import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon" import SkeletonShimmer from "@scandic-hotels/design-system/SkeletonShimmer" @@ -13,7 +14,6 @@ import useClickOutside from "@/hooks/useClickOutside" import { useHandleKeyUp } from "@/hooks/useHandleKeyUp" import { getInitials } from "@/utils/user" -import Avatar from "../Avatar" import MainMenuButton from "../MainMenuButton" import MyPagesMenuContent, { useMyPagesNavigation } from "../MyPagesMenuContent" diff --git a/apps/scandic-web/components/Header/MainMenu/MyPagesMenuWrapper/index.tsx b/apps/scandic-web/components/Header/MainMenu/MyPagesMenuWrapper/index.tsx index b0d0e8aa3..a70b1168f 100644 --- a/apps/scandic-web/components/Header/MainMenu/MyPagesMenuWrapper/index.tsx +++ b/apps/scandic-web/components/Header/MainMenu/MyPagesMenuWrapper/index.tsx @@ -4,13 +4,13 @@ import { useSession } from "next-auth/react" import { useIntl } from "react-intl" import { MembershipLevelEnum } from "@scandic-hotels/common/constants/membershipLevels" +import { Avatar } from "@scandic-hotels/design-system/Avatar" import { trpc } from "@scandic-hotels/trpc/client" import LoginButton from "@/components/LoginButton" import useLang from "@/hooks/useLang" import { isValidClientSession } from "@/utils/clientSession" -import Avatar from "../Avatar" import MyPagesMenu, { MyPagesMenuSkeleton } from "../MyPagesMenu" import MyPagesMobileMenu, { MyPagesMobileMenuSkeleton, diff --git a/apps/scandic-web/components/Header/MainMenu/MyPagesMobileMenu/index.tsx b/apps/scandic-web/components/Header/MainMenu/MyPagesMobileMenu/index.tsx index d2ead5b65..ac9a7cb74 100644 --- a/apps/scandic-web/components/Header/MainMenu/MyPagesMobileMenu/index.tsx +++ b/apps/scandic-web/components/Header/MainMenu/MyPagesMobileMenu/index.tsx @@ -5,12 +5,13 @@ import { Dialog, Modal } from "react-aria-components" import { useIntl } from "react-intl" import { useMediaQuery } from "usehooks-ts" +import { Avatar } from "@scandic-hotels/design-system/Avatar" + import useDropdownStore from "@/stores/main-menu" import { useHandleKeyUp } from "@/hooks/useHandleKeyUp" import { getInitials } from "@/utils/user" -import Avatar from "../Avatar" import MainMenuButton from "../MainMenuButton" import MyPagesMenuContent from "../MyPagesMenuContent" diff --git a/apps/scandic-web/components/MyPages/CopyMembershipIdButton/copyMembershipIdButton.module.css b/apps/scandic-web/components/MyPages/CopyMembershipIdButton/copyMembershipIdButton.module.css new file mode 100644 index 000000000..b35ca57ad --- /dev/null +++ b/apps/scandic-web/components/MyPages/CopyMembershipIdButton/copyMembershipIdButton.module.css @@ -0,0 +1,11 @@ +.container { + display: flex; + align-items: center; + gap: var(--Space-x1); + color: var(--Scandic-Red-100); +} + +.copyButton { + min-width: auto; + padding: var(--Space-x05); +} diff --git a/apps/scandic-web/components/MyPages/CopyMembershipIdButton/index.tsx b/apps/scandic-web/components/MyPages/CopyMembershipIdButton/index.tsx new file mode 100644 index 000000000..112e11673 --- /dev/null +++ b/apps/scandic-web/components/MyPages/CopyMembershipIdButton/index.tsx @@ -0,0 +1,56 @@ +"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 { Typography } from "@scandic-hotels/design-system/Typography" + +import { toast } from "@/components/TempDesignSystem/Toasts" + +import styles from "./copyMembershipIdButton.module.css" + +interface CopyMembershipIdButtonProps { + membershipNumber: string +} + +export default function CopyMembershipIdButton({ + membershipNumber, +}: CopyMembershipIdButtonProps) { + const intl = useIntl() + + function handleCopy() { + try { + navigator.clipboard.writeText(membershipNumber) + toast.success( + intl.formatMessage({ + defaultMessage: "Membership ID copied to clipboard", + }) + ) + } catch { + toast.error( + intl.formatMessage({ + defaultMessage: "Failed to copy", + }) + ) + } + } + + return ( +
+ + + {membershipNumber} + + + +
+ ) +} diff --git a/apps/scandic-web/env/server.ts b/apps/scandic-web/env/server.ts index da3c39ac8..545725c9f 100644 --- a/apps/scandic-web/env/server.ts +++ b/apps/scandic-web/env/server.ts @@ -160,6 +160,11 @@ export const env = createEnv({ .refine((s) => s === "1" || s === "0") .transform((s) => s === "1") .default("1"), + ENABLE_NEW_OVERVIEW_SECTION: z + .string() + .refine((s) => s === "true" || s === "false") + .transform((s) => s === "true") + .default("false"), }, emptyStringAsUndefined: true, runtimeEnv: { @@ -243,6 +248,7 @@ export const env = createEnv({ DTMC_ENTRA_ID_SECRET: process.env.DTMC_ENTRA_ID_SECRET, CAMPAIGN_PAGES_ENABLED: process.env.CAMPAIGN_PAGES_ENABLED, WEBVIEW_SHOW_OVERVIEW: process.env.WEBVIEW_SHOW_OVERVIEW, + ENABLE_NEW_OVERVIEW_SECTION: process.env.ENABLE_NEW_OVERVIEW_SECTION, }, }) diff --git a/packages/design-system/lib/components/Avatar/Avatar.stories.tsx b/packages/design-system/lib/components/Avatar/Avatar.stories.tsx new file mode 100644 index 000000000..27172b478 --- /dev/null +++ b/packages/design-system/lib/components/Avatar/Avatar.stories.tsx @@ -0,0 +1,125 @@ +import type { Meta, StoryObj } from '@storybook/nextjs-vite' + +import { Avatar } from '.' +import { config } from './variants' + +const meta: Meta = { + title: 'Components/Avatar', + component: Avatar, + parameters: { + layout: 'centered', + }, + argTypes: { + size: { + control: { type: 'select' }, + options: Object.keys(config.variants.size), + }, + }, +} + +export default meta + +type Story = StoryObj + +export const WithImage: Story = { + args: { + src: `../../../public/img/profile-picture.png`, + alt: 'Profile photo', + size: 'md', + }, +} + +export const WithInitials: Story = { + args: { + initials: 'FR', + size: 'md', + }, +} + +export const Fallback: Story = { + args: { + size: 'md', + }, +} + +export const SmallSize: Story = { + render: () => ( +
+ + + +
+ ), +} + +export const MediumSize: Story = { + render: () => ( +
+ + + +
+ ), +} + +export const LargeSize: Story = { + render: () => ( +
+ + + +
+ ), +} + +export const AllSizes: Story = { + render: () => ( +
+
+ + Small (20px) +
+
+ + Medium (32px) +
+
+ + Large (55px) +
+
+ ), +} diff --git a/packages/design-system/lib/components/Avatar/avatar.module.css b/packages/design-system/lib/components/Avatar/avatar.module.css new file mode 100644 index 000000000..5d9677d69 --- /dev/null +++ b/packages/design-system/lib/components/Avatar/avatar.module.css @@ -0,0 +1,38 @@ +.avatar { + display: flex; + justify-content: center; + align-items: center; + overflow: hidden; + border-radius: var(--Corner-radius-rounded); + background-color: var(--Overlay-40); +} + +.avatar:has(.initials) { + background-color: var(--Icon-Accent); +} + +.avatar img { + object-fit: cover; + width: 100%; + height: 100%; +} + +.size-sm { + width: 24px; + height: 24px; +} + +.size-md { + width: 32px; + height: 32px; +} + +.size-lg { + width: 55px; + height: 55px; +} + +.initials { + color: white; + text-transform: uppercase; +} diff --git a/packages/design-system/lib/components/Avatar/index.tsx b/packages/design-system/lib/components/Avatar/index.tsx new file mode 100644 index 000000000..b7edcb89d --- /dev/null +++ b/packages/design-system/lib/components/Avatar/index.tsx @@ -0,0 +1,35 @@ +import { MaterialIcon } from '../Icons/MaterialIcon' +import { Typography } from '../Typography' +import Image from '../Image' + +import { variants } from './variants' +import type { AvatarProps } from './types' + +export function Avatar({ + src, + alt, + initials, + size = 'md', + className, +}: AvatarProps) { + const classNames = variants({ size, className }) + const pixelSize = size === 'sm' ? 24 : size === 'md' ? 32 : 55 + const iconSize = size === 'sm' ? 16 : 24 + + return ( +
+ {src ? ( + {alt + ) : initials ? ( + + {initials} + + ) : ( + + )} +
+ ) +} diff --git a/packages/design-system/lib/components/Avatar/types.ts b/packages/design-system/lib/components/Avatar/types.ts new file mode 100644 index 000000000..4a821a306 --- /dev/null +++ b/packages/design-system/lib/components/Avatar/types.ts @@ -0,0 +1,23 @@ +export interface AvatarProps { + /** + * The URL of the image to display + */ + src?: string + /** + * Alt text for the image (for accessibility) + */ + alt?: string + /** + * Initials to display when no image is provided + */ + initials?: string | null + /** + * Size of the avatar + * @default 'md' + */ + size?: 'sm' | 'md' | 'lg' + /** + * Additional CSS class names + */ + className?: string +} diff --git a/packages/design-system/lib/components/Avatar/variants.ts b/packages/design-system/lib/components/Avatar/variants.ts new file mode 100644 index 000000000..58888eb17 --- /dev/null +++ b/packages/design-system/lib/components/Avatar/variants.ts @@ -0,0 +1,20 @@ +import { cva } from 'class-variance-authority' + +import styles from './avatar.module.css' + +export const config = { + variants: { + size: { + sm: styles['size-sm'], + md: styles['size-md'], + lg: styles['size-lg'], + }, + }, + defaultVariants: { + size: 'md', + }, +} as const + +export const variants = Object.assign(cva(styles.avatar, config), { + initials: styles.initials, +}) diff --git a/packages/design-system/package.json b/packages/design-system/package.json index a4da7d289..cec3025f3 100644 --- a/packages/design-system/package.json +++ b/packages/design-system/package.json @@ -6,6 +6,7 @@ "exports": { "./Accordion": "./lib/components/Accordion/index.tsx", "./Accordion/AccordionItem": "./lib/components/Accordion/AccordionItem/index.tsx", + "./Avatar": "./lib/components/Avatar/index.tsx", "./BackToTopButton": "./lib/components/BackToTopButton/index.tsx", "./Body": "./lib/components/Body/index.tsx", "./BookingCodeChip": "./lib/components/BookingCodeChip/index.tsx", diff --git a/packages/design-system/public/img/profile-picture.png b/packages/design-system/public/img/profile-picture.png new file mode 100644 index 000000000..6eff6bda2 Binary files /dev/null and b/packages/design-system/public/img/profile-picture.png differ