feat(LOY-198): dtmc card animation when hovering
This commit is contained in:
@@ -10,6 +10,8 @@ import { Typography } from "@scandic-hotels/design-system/Typography"
|
|||||||
import Modal from "@/components/Modal"
|
import Modal from "@/components/Modal"
|
||||||
import useWakeLock from "@/hooks/useWakeLock"
|
import useWakeLock from "@/hooks/useWakeLock"
|
||||||
|
|
||||||
|
import DigitalTeamMemberCardContent from "./Content"
|
||||||
|
|
||||||
import styles from "./digitalTeamMemberCard.module.css"
|
import styles from "./digitalTeamMemberCard.module.css"
|
||||||
|
|
||||||
import type { User } from "@/types/user"
|
import type { User } from "@/types/user"
|
||||||
@@ -56,66 +58,9 @@ export default function DigitalTeamMemberCardClient({
|
|||||||
{intl.formatMessage({ defaultMessage: "Scandic Family" })}
|
{intl.formatMessage({ defaultMessage: "Scandic Family" })}
|
||||||
</h2>
|
</h2>
|
||||||
</Typography>
|
</Typography>
|
||||||
<div className={styles.card}>
|
|
||||||
<div className={styles.content}>
|
|
||||||
<div className={styles.top}>
|
|
||||||
<Typography variant="Tag/sm">
|
|
||||||
<span>
|
|
||||||
{intl.formatMessage({ defaultMessage: "Team Member" })}
|
|
||||||
</span>
|
|
||||||
</Typography>
|
|
||||||
<Typography variant="Tag/sm">
|
|
||||||
{/* TODO: Should display country of employment */}
|
|
||||||
{/* eslint-disable-next-line formatjs/no-literal-string-in-jsx */}
|
|
||||||
<span>SWE</span>
|
|
||||||
</Typography>
|
|
||||||
</div>
|
|
||||||
<div className={styles.middle}>
|
|
||||||
<div className={styles.employeeNumber}>
|
|
||||||
<Typography variant="Title/sm">
|
|
||||||
{/* TODO: Should display employee number */}
|
|
||||||
{/* eslint-disable-next-line formatjs/no-literal-string-in-jsx */}
|
|
||||||
<div>123 456</div>
|
|
||||||
</Typography>
|
|
||||||
<svg
|
|
||||||
width="42"
|
|
||||||
height="42"
|
|
||||||
fill="none"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
className={styles.icon}
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
d="M3.5 35V12.288C3.5 8.933 3.5 7 4.268 7c.912 0 1.569 1.156 3.17 3.978l11.374 20.044C20.413 33.844 21.041 35 21.982 35c.768 0 .768-1.933.768-5.288V7M28 22.75h10.5M29.016 8.016c1.355-1.355 7.113-1.355 8.468 0 1.355 1.355 1.355 7.113 0 8.468-1.355 1.355-7.114 1.355-8.468 0-1.355-1.355-1.355-7.113 0-8.468Z"
|
|
||||||
strokeWidth="2.625"
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Typography variant="Title/md">
|
<DigitalTeamMemberCardContent user={user} />
|
||||||
<div>
|
|
||||||
{user.firstName} {user.lastName}
|
|
||||||
</div>
|
|
||||||
</Typography>
|
|
||||||
</div>
|
|
||||||
<div className={styles.bottom}>
|
|
||||||
<Typography variant="Tag/sm">
|
|
||||||
<span>
|
|
||||||
{/* TODO: Should display department of employment */}
|
|
||||||
{/* eslint-disable formatjs/no-literal-string-in-jsx */}
|
|
||||||
Haymarket by Scandic
|
|
||||||
{/* eslint-enable */}
|
|
||||||
</span>
|
|
||||||
</Typography>
|
|
||||||
<Typography variant="Tag/sm">
|
|
||||||
{/* TODO: Should display current state of employment */}
|
|
||||||
{/* eslint-disable-next-line formatjs/no-literal-string-in-jsx */}
|
|
||||||
<span>Employee</span>
|
|
||||||
</Typography>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<Typography variant="Body/Paragraph/mdRegular">
|
<Typography variant="Body/Paragraph/mdRegular">
|
||||||
<div className={styles.footer}>
|
<div className={styles.footer}>
|
||||||
{intl.formatMessage({
|
{intl.formatMessage({
|
||||||
|
|||||||
@@ -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<HTMLDivElement>(null)
|
||||||
|
const [isHovering, setIsHovering] = useState(false)
|
||||||
|
const [coords, setCoords] = useState({ x: 0, y: 0 })
|
||||||
|
const [rect, setRect] = useState<DOMRect>({
|
||||||
|
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<HTMLDivElement> | TouchEvent<HTMLDivElement>
|
||||||
|
) {
|
||||||
|
if (!ticking) {
|
||||||
|
animationFrame = requestAnimationFrame(update(evt))
|
||||||
|
}
|
||||||
|
ticking = true
|
||||||
|
}
|
||||||
|
|
||||||
|
function onInteractionMove(
|
||||||
|
evt: MouseEvent<HTMLDivElement> | TouchEvent<HTMLDivElement>
|
||||||
|
) {
|
||||||
|
requestTick(evt)
|
||||||
|
}
|
||||||
|
|
||||||
|
function update(
|
||||||
|
evt: MouseEvent<HTMLDivElement> | TouchEvent<HTMLDivElement>
|
||||||
|
) {
|
||||||
|
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 (
|
||||||
|
<div
|
||||||
|
className={styles.cardContainer}
|
||||||
|
onMouseEnter={handleInteractionStart}
|
||||||
|
onMouseMove={onInteractionMove}
|
||||||
|
onMouseLeave={handleInteractionEnd}
|
||||||
|
onTouchStart={handleInteractionStart}
|
||||||
|
onTouchMove={onInteractionMove}
|
||||||
|
onTouchEnd={handleInteractionEnd}
|
||||||
|
>
|
||||||
|
<div className={styles.card} ref={cardRef} style={renderSkew()}>
|
||||||
|
<div className={styles.shimmer} style={renderShimmer()} />
|
||||||
|
<div className={styles.content}>
|
||||||
|
<div className={styles.top}>
|
||||||
|
<Typography variant="Tag/sm">
|
||||||
|
<span>
|
||||||
|
{intl.formatMessage({ defaultMessage: "Team Member" })}
|
||||||
|
</span>
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="Tag/sm">
|
||||||
|
{/* TODO: Should display country of employment */}
|
||||||
|
{/* eslint-disable-next-line formatjs/no-literal-string-in-jsx */}
|
||||||
|
<span>SWE</span>
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
<div className={styles.middle}>
|
||||||
|
<div className={styles.employeeNumber}>
|
||||||
|
<Typography variant="Title/sm">
|
||||||
|
{/* TODO: Should display employee number */}
|
||||||
|
{/* eslint-disable-next-line formatjs/no-literal-string-in-jsx */}
|
||||||
|
<div>123 456</div>
|
||||||
|
</Typography>
|
||||||
|
<svg
|
||||||
|
width="42"
|
||||||
|
height="42"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
className={styles.icon}
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M3.5 35V12.288C3.5 8.933 3.5 7 4.268 7c.912 0 1.569 1.156 3.17 3.978l11.374 20.044C20.413 33.844 21.041 35 21.982 35c.768 0 .768-1.933.768-5.288V7M28 22.75h10.5M29.016 8.016c1.355-1.355 7.113-1.355 8.468 0 1.355 1.355 1.355 7.113 0 8.468-1.355 1.355-7.114 1.355-8.468 0-1.355-1.355-1.355-7.113 0-8.468Z"
|
||||||
|
strokeWidth="2.625"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Typography variant="Title/md">
|
||||||
|
<div>
|
||||||
|
{user.firstName} {user.lastName}
|
||||||
|
</div>
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
<div className={styles.bottom}>
|
||||||
|
<Typography variant="Tag/sm">
|
||||||
|
<span>
|
||||||
|
{/* TODO: Should display department of employment */}
|
||||||
|
{/* eslint-disable formatjs/no-literal-string-in-jsx */}
|
||||||
|
Haymarket by Scandic
|
||||||
|
{/* eslint-enable */}
|
||||||
|
</span>
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="Tag/sm">
|
||||||
|
{/* TODO: Should display current state of employment */}
|
||||||
|
{/* eslint-disable-next-line formatjs/no-literal-string-in-jsx */}
|
||||||
|
<span>Employee</span>
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -33,8 +33,22 @@
|
|||||||
color: var(--Text-Accent-Primary);
|
color: var(--Text-Accent-Primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.card {
|
.cardContainer {
|
||||||
position: relative;
|
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;
|
overflow: hidden;
|
||||||
border-radius: var(--Corner-radius-lg);
|
border-radius: var(--Corner-radius-lg);
|
||||||
box-shadow: 0 2px 1px rgb(255 255 255 / 11%) inset;
|
box-shadow: 0 2px 1px rgb(255 255 255 / 11%) inset;
|
||||||
@@ -44,6 +58,10 @@
|
|||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
color: var(--Text-Brand-OnPrimary-3-Accent);
|
color: var(--Text-Brand-OnPrimary-3-Accent);
|
||||||
background-color: var(--Surface-Brand-Primary-1-OnSurface-Default);
|
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 {
|
&::before {
|
||||||
content: "";
|
content: "";
|
||||||
@@ -54,7 +72,7 @@
|
|||||||
width: 360px;
|
width: 360px;
|
||||||
height: 360px;
|
height: 360px;
|
||||||
opacity: 0.3;
|
opacity: 0.3;
|
||||||
background: #e9aba3;
|
background-color: var(--Scandic-Peach-40);
|
||||||
box-shadow: 192px 192px 192px;
|
box-shadow: 192px 192px 192px;
|
||||||
border-radius: 9999px;
|
border-radius: 9999px;
|
||||||
filter: blur(96px);
|
filter: blur(96px);
|
||||||
@@ -76,7 +94,7 @@
|
|||||||
|
|
||||||
.content {
|
.content {
|
||||||
position: relative;
|
position: relative;
|
||||||
z-index: 3;
|
z-index: 4;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|||||||
Reference in New Issue
Block a user