feat(feed): spring/fill/burst animation on paw reactions

Adds a CSS-only spring-scale + expanding-burst-overlay animation
triggered when a pet paw-reacts to a post, respecting
prefers-reduced-motion. Only plays on the like transition, not unlike.

Gitea #15
This commit is contained in:
admin committed 2026-08-12 12:52:07 +02:00
1 parent 4d34b4cd3d
commit e39b3f1f6e
2 files changed
+67 -9

No files matched your search

+26 -9
View File
@@ -17,17 +17,24 @@ export function PawButton({ postId, petId, initialCount, initialReacted }: PawBu
const queryClient = useQueryClient();
const [reacted, setReacted] = useState(initialReacted);
const [count, setCount] = useState(initialCount);
const [animationKey, setAnimationKey] = useState(0);
const [showBurst, setShowBurst] = useState(false);
const toggleMutation = useMutation(
trpc.reactions.toggle.mutationOptions({
onMutate: () => {
const prevReacted = reacted;
const prevCount = count;
const nextReacted = !reacted;
// Optimistically flip the icon only — `reacted` can be stale (callers
// currently always pass initialReacted={false}), so count is corrected
// precisely from the server's actual `data.reacted` in onSuccess
// instead of guessing a +/-1 delta off a possibly-wrong prior state.
setReacted(!reacted);
setReacted(nextReacted);
if (nextReacted) {
setAnimationKey((key) => key + 1);
setShowBurst(true);
}
return { prevReacted, prevCount };
},
onSuccess: (data, _vars, context) => {
@@ -61,14 +68,24 @@ export function PawButton({ postId, petId, initialCount, initialReacted }: PawBu
aria-label={reacted ? "Remove paw" : "Paw this post"}
className="flex items-center gap-1.5 text-sm transition-colors disabled:opacity-60"
>
<PawPrint
className={`h-4 w-4 transition-colors ${
reacted
? "fill-orange-500 text-orange-500"
: "text-muted-foreground hover:text-orange-500"
}`}
aria-hidden="true"
/>
<span className="relative inline-flex h-4 w-4 items-center justify-center">
<PawPrint
key={animationKey}
className={`h-4 w-4 transition-colors ${
reacted
? "fill-orange-500 text-orange-500 animate-paw-spring"
: "text-muted-foreground hover:text-orange-500"
}`}
aria-hidden="true"
/>
{showBurst && (
<PawPrint
className="absolute inset-0 h-4 w-4 fill-orange-500 text-orange-500 animate-paw-burst"
aria-hidden="true"
onAnimationEnd={() => setShowBurst(false)}
/>
)}
</span>
{count > 0 && (
<span className={reacted ? "text-orange-500" : "text-muted-foreground"}>
{count}