diff --git a/src/app/globals.css b/src/app/globals.css index 6ab1d77..949cf0a 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -127,4 +127,45 @@ html { @apply font-sans; } +} + +@keyframes paw-spring { + 0% { + transform: scale(1); + } + 35% { + transform: scale(1.3); + } + 60% { + transform: scale(0.92); + } + 100% { + transform: scale(1); + } +} + +@keyframes paw-burst { + 0% { + transform: scale(0.6); + opacity: 0.9; + } + 100% { + transform: scale(2.1); + opacity: 0; + } +} + +.animate-paw-spring { + animation: paw-spring 0.4s cubic-bezier(0.34, 1.56, 0.64, 1); +} + +.animate-paw-burst { + animation: paw-burst 0.5s ease-out forwards; +} + +@media (prefers-reduced-motion: reduce) { + .animate-paw-spring, + .animate-paw-burst { + animation: none; + } } \ No newline at end of file diff --git a/src/components/feed/PawButton.tsx b/src/components/feed/PawButton.tsx index abddf30..b9d9b7f 100644 --- a/src/components/feed/PawButton.tsx +++ b/src/components/feed/PawButton.tsx @@ -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" > -