Replace react-spring with motion, fix breaking changes in react-window/date-fns-tz/pure-rand.

This commit is contained in:
Mike Cao committed 2026-04-18 15:41:25 -07:00
1 parent df29c16c00
commit cc829c8f6f
9 files changed
+132 -110

No files matched your search

+1 -1
View File
@@ -59,7 +59,6 @@
"@prisma/adapter-pg": "^7.7.0",
"@prisma/client": "^7.7.0",
"@prisma/extension-read-replicas": "^0.5.0",
"@react-spring/web": "^10.0.3",
"@svgr/cli": "^8.1.0",
"@tanstack/react-query": "^5.99.0",
"@umami/react-zen": "^0.245.0",
@@ -89,6 +88,7 @@
"kafkajs": "^2.1.0",
"lucide-react": "^1.8.0",
"maxmind": "^5.0.6",
"motion": "^12.38.0",
"next": "16.2.4",
"next-intl": "4.9.1",
"node-fetch": "^3.2.8",
+60 -62
View File
@@ -32,9 +32,6 @@ importers:
'@prisma/extension-read-replicas':
specifier: ^0.5.0
version: 0.5.0(@prisma/client@7.7.0(prisma@7.7.0(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(typescript@6.0.3))(typescript@6.0.3))
'@react-spring/web':
specifier: ^10.0.3
version: 10.0.3(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
'@svgr/cli':
specifier: ^8.1.0
version: 8.1.0(typescript@6.0.3)
@@ -122,6 +119,9 @@ importers:
maxmind:
specifier: ^5.0.6
version: 5.0.6
motion:
specifier: ^12.38.0
version: 12.38.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
next:
specifier: 16.2.4
version: 16.2.4(@babel/core@7.29.0)(babel-plugin-react-compiler@19.1.0-rc.2)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
@@ -2182,54 +2182,27 @@ packages:
react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1
react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1
'@react-spring/animated@10.0.3':
resolution: {integrity: sha512-7MrxADV3vaUADn2V9iYhaIL6iOWRx9nCJjYrsk2AHD2kwPr6fg7Pt0v+deX5RnCDmCKNnD6W5fasiyM8D+wzJQ==}
peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
'@react-spring/animated@9.7.5':
resolution: {integrity: sha512-Tqrwz7pIlsSDITzxoLS3n/v/YCUHQdOIKtOJf4yL6kYVSDTSmVK1LI1Q3M/uu2Sx4X3pIWF3xLUhlsA6SPNTNg==}
peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0
'@react-spring/core@10.0.3':
resolution: {integrity: sha512-D4DwNO68oohDf/0HG2G0Uragzb9IA1oXblxrd6MZAcBcUQG2EHUWXewjdECMPLNmQvlYVyyBRH6gPxXM5DX7DQ==}
peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
'@react-spring/core@9.7.5':
resolution: {integrity: sha512-rmEqcxRcu7dWh7MnCcMXLvrf6/SDlSokLaLTxiPlAYi11nN3B5oiCUAblO72o+9z/87j2uzxa2Inm8UbLjXA+w==}
peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0
'@react-spring/rafz@10.0.3':
resolution: {integrity: sha512-Ri2/xqt8OnQ2iFKkxKMSF4Nqv0LSWnxXT4jXFzBDsHgeeH/cHxTLupAWUwmV9hAGgmEhBmh5aONtj3J6R/18wg==}
'@react-spring/rafz@9.7.5':
resolution: {integrity: sha512-5ZenDQMC48wjUzPAm1EtwQ5Ot3bLIAwwqP2w2owG5KoNdNHpEJV263nGhCeKKmuA3vG2zLLOdu3or6kuDjA6Aw==}
'@react-spring/shared@10.0.3':
resolution: {integrity: sha512-geCal66nrkaQzUVhPkGomylo+Jpd5VPK8tPMEDevQEfNSWAQP15swHm+MCRG4wVQrQlTi9lOzKzpRoTL3CA84Q==}
peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
'@react-spring/shared@9.7.5':
resolution: {integrity: sha512-wdtoJrhUeeyD/PP/zo+np2s1Z820Ohr/BbuVYv+3dVLW7WctoiN7std8rISoYoHpUXtbkpesSKuPIw/6U1w1Pw==}
peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0
'@react-spring/types@10.0.3':
resolution: {integrity: sha512-H5Ixkd2OuSIgHtxuHLTt7aJYfhMXKXT/rK32HPD/kSrOB6q6ooeiWAXkBy7L8F3ZxdkBb9ini9zP9UwnEFzWgQ==}
'@react-spring/types@9.7.5':
resolution: {integrity: sha512-HVj7LrZ4ReHWBimBvu2SKND3cDVUPWKLqRTmWe/fNY6o1owGOX0cAHbdPDTMelgBlVbrTKrre6lFkhqGZErK/g==}
'@react-spring/web@10.0.3':
resolution: {integrity: sha512-ndU+kWY81rHsT7gTFtCJ6mrVhaJ6grFmgTnENipzmKqot4HGf5smPNK+cZZJqoGeDsj9ZsiWPW4geT/NyD484A==}
peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
'@react-spring/web@9.7.5':
resolution: {integrity: sha512-lmvqGwpe+CSttsWNZVr+Dg62adtKhauGwLyGE/RRyZ8AAMLgb9x3NDMA5RMElXo+IMyTkPp7nxTB8ZQlmhb6JQ==}
peerDependencies:
@@ -3987,6 +3960,20 @@ packages:
fraction.js@5.3.4:
resolution: {integrity: sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==}
framer-motion@12.38.0:
resolution: {integrity: sha512-rFYkY/pigbcswl1XQSb7q424kSTQ8q6eAC+YUsSKooHQYuLdzdHjrt6uxUC+PRAO++q5IS7+TamgIw1AphxR+g==}
peerDependencies:
'@emotion/is-prop-valid': '*'
react: ^18.0.0 || ^19.0.0
react-dom: ^18.0.0 || ^19.0.0
peerDependenciesMeta:
'@emotion/is-prop-valid':
optional: true
react:
optional: true
react-dom:
optional: true
fs-extra@8.1.0:
resolution: {integrity: sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==}
engines: {node: '>=6 <7 || >=8'}
@@ -4913,6 +4900,26 @@ packages:
resolution: {integrity: sha512-7e87vk0DdWT647wjcfEtWeMtjm+zVGqNohN/aeIymbUfjHQ2T4Sx5kM+1irVDBSloNC3CkGKxswdMoo8yhqTDg==}
engines: {node: '>=10', npm: '>=6'}
motion-dom@12.38.0:
resolution: {integrity: sha512-pdkHLD8QYRp8VfiNLb8xIBJis1byQ9gPT3Jnh2jqfFtAsWUA3dEepDlsWe/xMpO8McV+VdpKVcp+E+TGJEtOoA==}
motion-utils@12.36.0:
resolution: {integrity: sha512-eHWisygbiwVvf6PZ1vhaHCLamvkSbPIeAYxWUuL3a2PD/TROgE7FvfHWTIH4vMl798QLfMw15nRqIaRDXTlYRg==}
motion@12.38.0:
resolution: {integrity: sha512-uYfXzeHlgThchzwz5Te47dlv5JOUC7OB4rjJ/7XTUgtBZD8CchMN8qEJ4ZVsUmTyYA44zjV0fBwsiktRuFnn+w==}
peerDependencies:
'@emotion/is-prop-valid': '*'
react: ^18.0.0 || ^19.0.0
react-dom: ^18.0.0 || ^19.0.0
peerDependenciesMeta:
'@emotion/is-prop-valid':
optional: true
react:
optional: true
react-dom:
optional: true
ms@2.1.3:
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
@@ -8566,25 +8573,12 @@ snapshots:
react: 19.2.5
react-dom: 19.2.5(react@19.2.5)
'@react-spring/animated@10.0.3(react@19.2.5)':
dependencies:
'@react-spring/shared': 10.0.3(react@19.2.5)
'@react-spring/types': 10.0.3
react: 19.2.5
'@react-spring/animated@9.7.5(react@19.2.5)':
dependencies:
'@react-spring/shared': 9.7.5(react@19.2.5)
'@react-spring/types': 9.7.5
react: 19.2.5
'@react-spring/core@10.0.3(react@19.2.5)':
dependencies:
'@react-spring/animated': 10.0.3(react@19.2.5)
'@react-spring/shared': 10.0.3(react@19.2.5)
'@react-spring/types': 10.0.3
react: 19.2.5
'@react-spring/core@9.7.5(react@19.2.5)':
dependencies:
'@react-spring/animated': 9.7.5(react@19.2.5)
@@ -8592,35 +8586,16 @@ snapshots:
'@react-spring/types': 9.7.5
react: 19.2.5
'@react-spring/rafz@10.0.3': {}
'@react-spring/rafz@9.7.5': {}
'@react-spring/shared@10.0.3(react@19.2.5)':
dependencies:
'@react-spring/rafz': 10.0.3
'@react-spring/types': 10.0.3
react: 19.2.5
'@react-spring/shared@9.7.5(react@19.2.5)':
dependencies:
'@react-spring/rafz': 9.7.5
'@react-spring/types': 9.7.5
react: 19.2.5
'@react-spring/types@10.0.3': {}
'@react-spring/types@9.7.5': {}
'@react-spring/web@10.0.3(react-dom@19.2.5(react@19.2.5))(react@19.2.5)':
dependencies:
'@react-spring/animated': 10.0.3(react@19.2.5)
'@react-spring/core': 10.0.3(react@19.2.5)
'@react-spring/shared': 10.0.3(react@19.2.5)
'@react-spring/types': 10.0.3
react: 19.2.5
react-dom: 19.2.5(react@19.2.5)
'@react-spring/web@9.7.5(react-dom@19.2.5(react@19.2.5))(react@19.2.5)':
dependencies:
'@react-spring/animated': 9.7.5(react@19.2.5)
@@ -10410,6 +10385,15 @@ snapshots:
fraction.js@5.3.4: {}
framer-motion@12.38.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5):
dependencies:
motion-dom: 12.38.0
motion-utils: 12.36.0
tslib: 2.8.1
optionalDependencies:
react: 19.2.5
react-dom: 19.2.5(react@19.2.5)
fs-extra@8.1.0:
dependencies:
graceful-fs: 4.2.11
@@ -11524,6 +11508,20 @@ snapshots:
mmdb-lib@3.0.2: {}
motion-dom@12.38.0:
dependencies:
motion-utils: 12.36.0
motion-utils@12.36.0: {}
motion@12.38.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5):
dependencies:
framer-motion: 12.38.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
tslib: 2.8.1
optionalDependencies:
react: 19.2.5
react-dom: 19.2.5(react@19.2.5)
ms@2.1.3: {}
mysql2@3.15.3:
+2 -2
View File
@@ -1,3 +1,3 @@
import { type AnimatedComponent, animated } from '@react-spring/web';
import { motion } from 'motion/react';
export const AnimatedDiv: AnimatedComponent<any> = animated.div;
export const AnimatedDiv = motion.div;
+5 -5
View File
@@ -1,4 +1,4 @@
import { formatInTimeZone, utcToZonedTime, zonedTimeToUtc } from 'date-fns-tz';
import { formatInTimeZone, fromZonedTime, toZonedTime } from 'date-fns-tz';
import { TIMEZONE_CONFIG, TIMEZONE_LEGACY } from '@/lib/constants';
import { getTimezone } from '@/lib/date';
import { setItem } from '@/lib/storage';
@@ -61,19 +61,19 @@ export function useTimezone() {
};
const toUtc = (date: Date | string | number) => {
return zonedTimeToUtc(date, timezone);
return fromZonedTime(date, timezone);
};
const fromUtc = (date: Date | string | number) => {
return utcToZonedTime(date, timezone);
return toZonedTime(date, timezone);
};
const localToUtc = (date: Date | string | number) => {
return zonedTimeToUtc(date, localTimeZone);
return fromZonedTime(date, localTimeZone);
};
const localFromUtc = (date: Date | string | number) => {
return utcToZonedTime(date, localTimeZone);
return toZonedTime(date, localTimeZone);
};
const canonicalizeTimezone = (timezone: string): string => {
+29 -22
View File
@@ -1,7 +1,7 @@
import { config, useSpring } from '@react-spring/web';
import { Column, Grid, Row, Text } from '@umami/react-zen';
import type { ReactNode } from 'react';
import { FixedSizeList } from 'react-window';
import { useSpring, useTransform } from 'motion/react';
import { type ReactNode, useEffect } from 'react';
import { List, type RowComponentProps } from 'react-window';
import { AnimatedDiv } from '@/components/common/AnimatedDiv';
import { Empty } from '@/components/common/Empty';
import { useMessages, useMobile } from '@/components/hooks';
@@ -62,7 +62,7 @@ export function ListTable({
);
};
const ListTableRow = ({ index, style }) => {
const ListTableRow = ({ index, style }: RowComponentProps) => {
return <div style={style}>{getRow(data[index], index)}</div>;
};
@@ -82,14 +82,14 @@ export function ListTable({
<Column gap="1">
{data?.length === 0 && <Empty />}
{virtualize && data.length > 0 ? (
<FixedSizeList
width="100%"
height={itemCount * ITEM_SIZE}
itemCount={data.length}
itemSize={ITEM_SIZE}
>
{ListTableRow}
</FixedSizeList>
<List
style={{ width: '100%', height: itemCount * ITEM_SIZE }}
defaultHeight={itemCount * ITEM_SIZE}
rowCount={data.length}
rowHeight={ITEM_SIZE}
rowComponent={ListTableRow}
rowProps={{}}
/>
) : (
data.map(getRow)
)}
@@ -108,12 +108,21 @@ const AnimatedRow = ({
formatCount,
isPhone,
}) => {
const props = useSpring({
width: percent,
y: !Number.isNaN(value) ? value : 0,
from: { width: 0, y: 0 },
config: animate ? config.default : { duration: 0 },
});
const y = !Number.isNaN(value) ? value : 0;
const ySpring = useSpring(0, { stiffness: 170, damping: 26 });
const widthSpring = useSpring(0, { stiffness: 170, damping: 26 });
const yText = useTransform(ySpring, n => (formatCount ? formatCount(n) : formatLongNumber(n)));
const widthText = useTransform(widthSpring, n => `${n?.toFixed?.(0)}%`);
useEffect(() => {
if (animate) {
ySpring.set(y);
widthSpring.set(percent);
} else {
ySpring.jump(y);
widthSpring.jump(percent);
}
}, [y, percent, animate, ySpring, widthSpring]);
return (
<Grid
@@ -136,9 +145,7 @@ const AnimatedRow = ({
>
{change}
<Text weight="bold">
<AnimatedDiv title={props?.y as any}>
{formatCount ? props.y?.to(formatCount) : props.y?.to(formatLongNumber)}
</AnimatedDiv>
<AnimatedDiv title={String(value)}>{yText}</AnimatedDiv>
</Text>
</Row>
{showPercentage && (
@@ -151,7 +158,7 @@ const AnimatedRow = ({
color="muted"
paddingLeft="3"
>
<AnimatedDiv>{props.width.to(n => `${n?.toFixed?.(0)}%`)}</AnimatedDiv>
<AnimatedDiv>{widthText}</AnimatedDiv>
</Row>
)}
</Grid>
+18 -6
View File
@@ -1,6 +1,6 @@
import { useSpring } from '@react-spring/web';
import { Button, Column, Icon, Row, Text, Tooltip, TooltipTrigger } from '@umami/react-zen';
import type { ReactNode } from 'react';
import { useSpring, useTransform } from 'motion/react';
import { type ReactNode, useEffect } from 'react';
import { AnimatedDiv } from '@/components/common/AnimatedDiv';
import { Info } from '@/components/icons';
import { ChangeLabel } from '@/components/metrics/ChangeLabel';
@@ -30,8 +30,20 @@ export const MetricCard = ({
}: MetricCardProps) => {
const diff = value - change;
const pct = diff !== 0 ? ((value - diff) / diff) * 100 : value !== 0 ? 100 : 0;
const props = useSpring({ x: Number(value) || 0, from: { x: 0 } });
const changeProps = useSpring({ x: Number(pct) || 0, from: { x: 0 } });
const x = Number(value) || 0;
const p = Number(pct) || 0;
const xSpring = useSpring(0, { stiffness: 170, damping: 26 });
const pctSpring = useSpring(0, { stiffness: 170, damping: 26 });
const valueText = useTransform(xSpring, n => formatValue(n));
const pctText = useTransform(pctSpring, n => `${Math.abs(~~n)}%`);
useEffect(() => {
xSpring.set(x);
}, [x, xSpring]);
useEffect(() => {
pctSpring.set(p);
}, [p, pctSpring]);
return (
<Column
@@ -61,11 +73,11 @@ export const MetricCard = ({
</Row>
)}
<Text size="4xl" weight="bold" wrap="nowrap">
<AnimatedDiv title={value?.toString()}>{props?.x?.to(x => formatValue(x))}</AnimatedDiv>
<AnimatedDiv title={value?.toString()}>{valueText}</AnimatedDiv>
</Text>
{showChange && (
<ChangeLabel value={change} title={formatValue(change)} reverseColors={reverseColors}>
<AnimatedDiv>{changeProps?.x?.to(x => `${Math.abs(~~x)}%`)}</AnimatedDiv>
<AnimatedDiv>{pctText}</AnimatedDiv>
</ChangeLabel>
)}
</Column>
+11 -7
View File
@@ -1,6 +1,6 @@
import { config, useSpring } from '@react-spring/web';
import { Column, Text } from '@umami/react-zen';
import { useRef } from 'react';
import { useSpring, useTransform } from 'motion/react';
import { useEffect, useRef } from 'react';
import { AnimatedDiv } from '@/components/common/AnimatedDiv';
import { Badge } from '@/components/common/Badge';
import { useMessages } from '@/components/hooks';
@@ -45,10 +45,14 @@ export const PerformanceCard = ({
const metricChanged = prevMetricRef.current !== metric;
if (metricChanged) prevMetricRef.current = metric;
const spring = useSpring({
value: !Number.isNaN(value) ? value : 0,
config: metricChanged ? { duration: 0 } : config.default,
});
const target = !Number.isNaN(value) ? value : 0;
const spring = useSpring(target, { stiffness: 170, damping: 26 });
const display = useTransform(spring, n => formatValue(n));
useEffect(() => {
if (metricChanged) spring.jump(target);
else spring.set(target);
}, [target, metricChanged, spring]);
return (
<Column
@@ -67,7 +71,7 @@ export const PerformanceCard = ({
{label}
</Text>
<Text size="4xl" weight="bold" wrap="nowrap">
<AnimatedDiv>{spring.value.to(n => formatValue(n))}</AnimatedDiv>
<AnimatedDiv>{display}</AnimatedDiv>
</Text>
<Badge variant={RATING_VARIANTS[rating]}>
{t(labels[rating === 'needs-improvement' ? 'needsImprovement' : rating])}
+2 -2
View File
@@ -38,7 +38,7 @@ import {
subWeeks,
subYears,
} from 'date-fns';
import { utcToZonedTime } from 'date-fns-tz';
import { toZonedTime } from 'date-fns-tz';
import { getDateLocale } from '@/lib/lang';
import type { DateRange } from '@/lib/types';
@@ -164,7 +164,7 @@ export function parseDateRange(
}
const date = new Date();
const now = timezone ? utcToZonedTime(date, timezone) : date;
const now = timezone ? toZonedTime(date, timezone) : date;
const dateLocale = getDateLocale(locale);
const { num = 1, unit } = parseDateValue(value);
+4 -3
View File
@@ -1,10 +1,11 @@
import prand from 'pure-rand';
import { uniformInt } from 'pure-rand/distribution/uniformInt';
import { xoroshiro128plus } from 'pure-rand/generator/xoroshiro128plus';
const seed = Date.now() ^ (Math.random() * 0x100000000);
const rng = prand.xoroshiro128plus(seed);
const rng = xoroshiro128plus(seed);
export function random(min: number, max: number) {
return prand.unsafeUniformIntDistribution(min, max, rng);
return uniformInt(rng, min, max);
}
export function getRandomChars(