Skip to content

Commit e393624

Browse files
committed
claude:feat: smoother adding of new activity cards
1 parent e16d7c3 commit e393624

2 files changed

Lines changed: 59 additions & 5 deletions

File tree

src/assets/css/custom.css

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,21 @@
162162
border-color: rgba(255, 255, 255, 0.2);
163163
}
164164

165+
@keyframes slideInCard {
166+
from {
167+
opacity: 0;
168+
transform: translateX(-40px) scale(0.95);
169+
}
170+
to {
171+
opacity: 1;
172+
transform: translateX(0) scale(1);
173+
}
174+
}
175+
176+
.activity-card-new {
177+
animation: slideInCard 0.5s cubic-bezier(0.22, 1, 0.36, 1) both;
178+
}
179+
165180
.activity-card-top {
166181
display: flex;
167182
align-items: center;

src/components/ui/recentActivity.tsx

Lines changed: 44 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useRef } from "react"
1+
import { useEffect, useRef, useState } from "react"
22
import { SpinnerCircular } from "spinners-react"
33
import { ApiResponseDto_PageStatsDto, PageActivityDto } from "~/backendApi"
44
import { Formatter } from "~/utils/misc/formatter"
@@ -44,15 +44,50 @@ function chainToAddressUrl(chain: number, protocol: number, address: string): st
4444
}
4545
}
4646

47+
function itemKey(item: PageActivityDto): string {
48+
return `${item.type}-${item.transaction}`
49+
}
50+
4751
const RecentActivity = ({ data, isLoading }: {
4852
data: ApiResponseDto_PageStatsDto, isLoading: boolean, error: string
4953
}) => {
5054
const scrollRef = useRef<HTMLDivElement>(null)
55+
const knownKeysRef = useRef<Set<string> | null>(null)
56+
const [newKeys, setNewKeys] = useState<Set<string>>(new Set())
5157

5258
let items: PageActivityDto[] | null = null
5359
if (data?.data != null) {
5460
items = [...(data.data.activity ?? [])].sort((a, b) => b.timestamp - a.timestamp)
55-
} else if (isLoading) {
61+
}
62+
63+
useEffect(() => {
64+
if (!items) return
65+
const currentKeys = new Set(items.map(itemKey))
66+
67+
if (knownKeysRef.current !== null) {
68+
const added = new Set<string>()
69+
for (const key of currentKeys) {
70+
if (!knownKeysRef.current.has(key)) added.add(key)
71+
}
72+
if (added.size > 0) {
73+
setNewKeys(added)
74+
// Clear animation class after animation ends
75+
const timer = setTimeout(() => setNewKeys(new Set()), 600)
76+
return () => clearTimeout(timer)
77+
}
78+
}
79+
80+
knownKeysRef.current = currentKeys
81+
}, [items])
82+
83+
// Also update known keys when newKeys clears (after animation)
84+
useEffect(() => {
85+
if (newKeys.size === 0 && items) {
86+
knownKeysRef.current = new Set(items.map(itemKey))
87+
}
88+
}, [newKeys, items])
89+
90+
if (!items && isLoading) {
5691
return <div style={{ textAlign: 'center' }}>
5792
<SpinnerCircular color={C.PAGE_COLOR_CODE} size={40} />
5893
</div>
@@ -76,7 +111,11 @@ const RecentActivity = ({ data, isLoading }: {
76111
</div>
77112
<div className="activity-carousel" ref={scrollRef}>
78113
{items.map((item, i) =>
79-
<ActivityCard key={`${item.type}-${item.transaction}-${i}`} activity={item} />
114+
<ActivityCard
115+
key={`${itemKey(item)}-${i}`}
116+
activity={item}
117+
isNew={newKeys.has(itemKey(item))}
118+
/>
80119
)}
81120
</div>
82121
</>
@@ -87,14 +126,14 @@ const ACTIVITY_CONFIG = {
87126
[PageActivityDto.type.DELEGATION]: { label: 'Delegated', cssType: 'delegated', cssAmount: 'delegation', addrLabel: 'By' },
88127
}
89128

90-
const ActivityCard = ({ activity }: { activity: PageActivityDto }) => {
129+
const ActivityCard = ({ activity, isNew }: { activity: PageActivityDto, isNew?: boolean }) => {
91130
const config = ACTIVITY_CONFIG[activity.type]
92131
const logo = CHAIN_LOGO[activity.chain]
93132
const symbol = C.CHAIN_SYMBOL[activity.chain]
94133
const txUrl = chainToTransactionUrl(activity.chain, activity.protocol, activity.transaction)
95134
const addrUrl = chainToAddressUrl(activity.chain, activity.protocol, activity.delegator)
96135

97-
return <div className="activity-card">
136+
return <div className={`activity-card${isNew ? ' activity-card-new' : ''}`}>
98137
<div className="activity-card-top">
99138
<img src={logo} width={28} alt={symbol} />
100139
<span className="activity-protocol">{C.PROTOCOL_NAME[activity.protocol]}</span>

0 commit comments

Comments
 (0)