From d53b07b9075e38c1d38f7fa5e52aaf46afc9fee9 Mon Sep 17 00:00:00 2001 From: Christian Andolf Date: Thu, 8 May 2025 11:11:30 +0200 Subject: [PATCH] feat(LOY-198): dtmc card animation when hovering --- .../MyPages/DigitalTeamMemberCard/Client.tsx | 63 +----- .../MyPages/DigitalTeamMemberCard/Content.tsx | 196 ++++++++++++++++++ .../digitalTeamMemberCard.module.css | 24 ++- 3 files changed, 221 insertions(+), 62 deletions(-) create mode 100644 apps/scandic-web/components/MyPages/DigitalTeamMemberCard/Content.tsx diff --git a/apps/scandic-web/components/MyPages/DigitalTeamMemberCard/Client.tsx b/apps/scandic-web/components/MyPages/DigitalTeamMemberCard/Client.tsx index c1ed47aae..8141d4c29 100644 --- a/apps/scandic-web/components/MyPages/DigitalTeamMemberCard/Client.tsx +++ b/apps/scandic-web/components/MyPages/DigitalTeamMemberCard/Client.tsx @@ -10,6 +10,8 @@ import { Typography } from "@scandic-hotels/design-system/Typography" import Modal from "@/components/Modal" import useWakeLock from "@/hooks/useWakeLock" +import DigitalTeamMemberCardContent from "./Content" + import styles from "./digitalTeamMemberCard.module.css" import type { User } from "@/types/user" @@ -56,66 +58,9 @@ export default function DigitalTeamMemberCardClient({ {intl.formatMessage({ defaultMessage: "Scandic Family" })} -
-
-
- - - {intl.formatMessage({ defaultMessage: "Team Member" })} - - - - {/* TODO: Should display country of employment */} - {/* eslint-disable-next-line formatjs/no-literal-string-in-jsx */} - SWE - -
-
-
- - {/* TODO: Should display employee number */} - {/* eslint-disable-next-line formatjs/no-literal-string-in-jsx */} -
123 456
-
- - - -
- -
- {user.firstName} {user.lastName} -
-
-
-
- - - {/* TODO: Should display department of employment */} - {/* eslint-disable formatjs/no-literal-string-in-jsx */} - Haymarket by Scandic - {/* eslint-enable */} - - - - {/* TODO: Should display current state of employment */} - {/* eslint-disable-next-line formatjs/no-literal-string-in-jsx */} - Employee - -
-
-
+ +
{intl.formatMessage({ diff --git a/apps/scandic-web/components/MyPages/DigitalTeamMemberCard/Content.tsx b/apps/scandic-web/components/MyPages/DigitalTeamMemberCard/Content.tsx new file mode 100644 index 000000000..0b880784d --- /dev/null +++ b/apps/scandic-web/components/MyPages/DigitalTeamMemberCard/Content.tsx @@ -0,0 +1,196 @@ +import { + type CSSProperties, + type MouseEvent, + type TouchEvent, + useEffect, + useRef, + useState, +} from "react" +import { useIntl } from "react-intl" + +import { Typography } from "@scandic-hotels/design-system/Typography" + +import { debounce } from "@/utils/debounce" + +import styles from "./digitalTeamMemberCard.module.css" + +import type { User } from "@/types/user" + +interface DigitalTeamMemberCardCardProps { + user: User +} + +export default function DigitalTeamMemberCardContent({ + user, +}: DigitalTeamMemberCardCardProps) { + const intl = useIntl() + const cardRef = useRef(null) + const [isHovering, setIsHovering] = useState(false) + const [coords, setCoords] = useState({ x: 0, y: 0 }) + const [rect, setRect] = useState({ + top: 0, + left: 0, + bottom: 0, + right: 0, + height: 0, + width: 0, + x: 0, + y: 0, + toJSON() {}, + }) + let ticking = false + let animationFrame = 0 + + useEffect(() => { + const observer = new ResizeObserver( + debounce(() => { + const el = cardRef.current + if (!el) return + const rect = el.getBoundingClientRect() + setRect(rect) + }) + ) + observer.observe(document.body) + return () => { + observer.unobserve(document.body) + } + }, []) + + function requestTick( + evt: MouseEvent | TouchEvent + ) { + if (!ticking) { + animationFrame = requestAnimationFrame(update(evt)) + } + ticking = true + } + + function onInteractionMove( + evt: MouseEvent | TouchEvent + ) { + requestTick(evt) + } + + function update( + evt: MouseEvent | TouchEvent + ) { + return () => { + let x, y + if ("touches" in evt) { + x = evt.touches[0].clientX - rect.left + y = evt.touches[0].clientY - rect.top + } else { + x = evt.clientX - rect.left + y = evt.clientY - rect.top + } + setCoords({ x, y }) + } + } + + function renderShimmer(): CSSProperties { + const { x, y } = coords + const oppositeX = 100 - (x / rect.width) * 100 + const oppositeY = 100 - (y / rect.height) * 100 + return { + background: `radial-gradient(circle at ${oppositeX}% ${oppositeY}%, rgb(233 171 163 / 40%) 0%, rgb(255 255 255 / 0%) 50%)`, + opacity: isHovering ? 1 : 0, + } + } + + function renderSkew(): CSSProperties { + const { x, y } = coords + const centerX = rect.width / 2 + const centerY = rect.height / 2 + const rotateY = ((x - centerX) / centerX) * 5 // Max 5 degrees + const rotateX = ((centerY - y) / centerY) * 5 // Max 5 degrees + + return { + transform: isHovering + ? `rotateX(${rotateX}deg) rotateY(${rotateY}deg)` + : "rotateX(0) rotateY(0)", + } + } + + function handleInteractionStart() { + setIsHovering(true) + } + + function handleInteractionEnd() { + setIsHovering(false) + cancelAnimationFrame(animationFrame) + } + + return ( +
+
+
+
+
+ + + {intl.formatMessage({ defaultMessage: "Team Member" })} + + + + {/* TODO: Should display country of employment */} + {/* eslint-disable-next-line formatjs/no-literal-string-in-jsx */} + SWE + +
+
+
+ + {/* TODO: Should display employee number */} + {/* eslint-disable-next-line formatjs/no-literal-string-in-jsx */} +
123 456
+
+ + + +
+ + +
+ {user.firstName} {user.lastName} +
+
+
+
+ + + {/* TODO: Should display department of employment */} + {/* eslint-disable formatjs/no-literal-string-in-jsx */} + Haymarket by Scandic + {/* eslint-enable */} + + + + {/* TODO: Should display current state of employment */} + {/* eslint-disable-next-line formatjs/no-literal-string-in-jsx */} + Employee + +
+
+
+
+ ) +} diff --git a/apps/scandic-web/components/MyPages/DigitalTeamMemberCard/digitalTeamMemberCard.module.css b/apps/scandic-web/components/MyPages/DigitalTeamMemberCard/digitalTeamMemberCard.module.css index 9487632bf..ea901ca3f 100644 --- a/apps/scandic-web/components/MyPages/DigitalTeamMemberCard/digitalTeamMemberCard.module.css +++ b/apps/scandic-web/components/MyPages/DigitalTeamMemberCard/digitalTeamMemberCard.module.css @@ -33,8 +33,22 @@ color: var(--Text-Accent-Primary); } -.card { +.cardContainer { position: relative; + perspective: 1000px; + transform-style: preserve-3d; +} + +.shimmer { + position: absolute; + inset: 0; + z-index: 3; + opacity: 0; + transition: opacity 0.3s ease; + mix-blend-mode: overlay; +} + +.card { overflow: hidden; border-radius: var(--Corner-radius-lg); box-shadow: 0 2px 1px rgb(255 255 255 / 11%) inset; @@ -44,6 +58,10 @@ max-width: 100%; color: var(--Text-Brand-OnPrimary-3-Accent); background-color: var(--Surface-Brand-Primary-1-OnSurface-Default); + box-shadow: 0 4px 44px rgb(0 0 0 / 25%); + transition: transform 0.3s ease-out; + will-change: transform; + user-select: none; &::before { content: ""; @@ -54,7 +72,7 @@ width: 360px; height: 360px; opacity: 0.3; - background: #e9aba3; + background-color: var(--Scandic-Peach-40); box-shadow: 192px 192px 192px; border-radius: 9999px; filter: blur(96px); @@ -76,7 +94,7 @@ .content { position: relative; - z-index: 3; + z-index: 4; height: 100%; display: flex; flex-direction: column;