-
Notifications
You must be signed in to change notification settings - Fork 23
ux: Add animated success celebration when settled up #270
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,52 @@ | ||
| import { motion } from 'framer-motion'; | ||
| import { Check } from 'lucide-react'; | ||
| import React from 'react'; | ||
| import { THEMES } from '../constants'; | ||
| import { useTheme } from '../contexts/ThemeContext'; | ||
| import { Confetti } from './ui/Confetti'; | ||
|
|
||
| export const SettledUpState = () => { | ||
| const { style } = useTheme(); | ||
|
|
||
| return ( | ||
| <div className="col-span-full flex flex-col items-center justify-center py-20 relative"> | ||
| <Confetti count={100} /> | ||
|
|
||
| <motion.div | ||
| initial={{ scale: 0, rotate: -180 }} | ||
| animate={{ scale: 1, rotate: 0 }} | ||
| transition={{ | ||
| type: "spring", | ||
| stiffness: 260, | ||
| damping: 20, | ||
| delay: 0.1 | ||
| }} | ||
| className={`w-24 h-24 flex items-center justify-center mx-auto mb-6 ${ | ||
| style === THEMES.NEOBRUTALISM | ||
| ? 'bg-emerald-100 border-2 border-black rounded-none shadow-[4px_4px_0px_0px_rgba(0,0,0,1)]' | ||
| : 'bg-emerald-100 dark:bg-emerald-900/20 rounded-full ring-4 ring-emerald-500/20' | ||
| }`} | ||
| > | ||
| <motion.div | ||
| initial={{ opacity: 0, scale: 0.5 }} | ||
| animate={{ opacity: 1, scale: 1 }} | ||
| transition={{ delay: 0.2 }} | ||
| > | ||
| <Check size={48} strokeWidth={3} className={style === THEMES.NEOBRUTALISM ? 'text-black' : 'text-emerald-500'} /> | ||
| </motion.div> | ||
| </motion.div> | ||
|
|
||
| <motion.div | ||
| initial={{ opacity: 0, y: 20 }} | ||
| animate={{ opacity: 1, y: 0 }} | ||
| transition={{ delay: 0.3 }} | ||
| className="text-center" | ||
| > | ||
| <h3 className={`text-3xl font-black mb-2 ${style === THEMES.NEOBRUTALISM ? 'text-black' : 'text-emerald-500'}`}> | ||
| All Settled Up! | ||
| </h3> | ||
| <p className="opacity-60 text-lg">No outstanding balances in this group.</p> | ||
| </motion.div> | ||
| </div> | ||
| ); | ||
| }; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,65 @@ | ||
| import { motion } from 'framer-motion'; | ||
| import React, { useEffect, useState } from 'react'; | ||
| import { COLORS } from '../../constants'; | ||
|
|
||
| interface ConfettiProps { | ||
| count?: number; | ||
| } | ||
|
|
||
| interface Particle { | ||
| id: number; | ||
| x: number; | ||
| y: number; | ||
| color: string; | ||
| size: number; | ||
| rotation: number; | ||
| duration: number; | ||
| delay: number; | ||
| } | ||
|
Comment on lines
+9
to
+18
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Stabilize particle randomness to avoid jitter on re-renders.
💡 Proposed fix interface Particle {
id: number;
x: number;
y: number;
color: string;
size: number;
rotation: number;
duration: number;
delay: number;
+ drift: number;
+ isCircle: boolean;
}
@@
const newParticles: Particle[] = Array.from({ length: count }).map((_, i) => ({
id: i,
x: Math.random() * 100, // percent
y: -10, // start above
color: COLORS[Math.floor(Math.random() * COLORS.length)],
size: Math.random() * 8 + 4,
rotation: Math.random() * 360,
duration: Math.random() * 2 + 2,
delay: Math.random() * 0.5,
+ drift: Math.random() * 20 - 10,
+ isCircle: Math.random() > 0.5,
}));
@@
animate={{
y: '105vh',
rotate: p.rotation + 720,
- x: `${p.x + (Math.random() * 20 - 10)}vw`,
+ x: `${p.x + p.drift}vw`,
opacity: 0,
}}
@@
style={{
position: 'absolute',
width: p.size,
height: p.size,
backgroundColor: p.color,
- borderRadius: Math.random() > 0.5 ? '50%' : '0%',
+ borderRadius: p.isCircle ? '50%' : '0%',
}}Also applies to: 24-60 🤖 Prompt for AI Agents |
||
|
|
||
| export const Confetti = ({ count = 50 }: ConfettiProps) => { | ||
| const [particles, setParticles] = useState<Particle[]>([]); | ||
|
|
||
| useEffect(() => { | ||
| const newParticles: Particle[] = Array.from({ length: count }).map((_, i) => ({ | ||
| id: i, | ||
| x: Math.random() * 100, // percent | ||
| y: -10, // start above | ||
| color: COLORS[Math.floor(Math.random() * COLORS.length)], | ||
| size: Math.random() * 8 + 4, | ||
| rotation: Math.random() * 360, | ||
| duration: Math.random() * 2 + 2, | ||
| delay: Math.random() * 0.5, | ||
| })); | ||
| setParticles(newParticles); | ||
| }, [count]); | ||
|
|
||
| return ( | ||
| <div className="fixed inset-0 pointer-events-none z-50 overflow-hidden" aria-hidden="true"> | ||
| {particles.map((p) => ( | ||
| <motion.div | ||
| key={p.id} | ||
| initial={{ y: '-5vh', x: `${p.x}vw`, rotate: p.rotation, opacity: 1 }} | ||
| animate={{ | ||
| y: '105vh', | ||
| rotate: p.rotation + 720, | ||
| x: `${p.x + (Math.random() * 20 - 10)}vw`, | ||
| opacity: 0, | ||
| }} | ||
| transition={{ | ||
| duration: p.duration, | ||
| delay: p.delay, | ||
| ease: 'easeOut', | ||
| }} | ||
| style={{ | ||
| position: 'absolute', | ||
| width: p.size, | ||
| height: p.size, | ||
| backgroundColor: p.color, | ||
| borderRadius: Math.random() > 0.5 ? '50%' : '0%', | ||
| }} | ||
| /> | ||
| ))} | ||
| </div> | ||
| ); | ||
| }; | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧹 Nitpick | 🔵 Trivial
🧩 Analysis chain
🏁 Script executed:
Repository: Devasy/splitwiser
Length of output: 79
🏁 Script executed:
Repository: Devasy/splitwiser
Length of output: 105
🏁 Script executed:
Repository: Devasy/splitwiser
Length of output: 93
🌐 Web query:
framer-motion v11 useReducedMotion API prefers-reduced-motion💡 Result:
In Framer Motion v11, reduced-motion support is built around the user’s OS/browser setting exposed via the CSS media query
(prefers-reduced-motion: reduce). [3]1) Manual:
useReducedMotion()useReducedMotion()is a hook that returns a boolean indicating whether Reduced Motion is enabled, and it will re-render when the setting changes. [2][1][2]
2) Automatic (site-wide):
<MotionConfig reducedMotion="user" />If you want Motion to automatically comply across your app, set:
With
"user", Motion will disable transform and layout animations while preserving other animations likeopacity. [1]Sources: Motion accessibility guide + API docs [1][2], MDN
prefers-reduced-motion[3].[1] https://motion.dev/docs/react-accessibility
[2] https://motion.dev/motion/use-reduced-motion/
[3] https://developer.mozilla.org/en-US/docs/Web/CSS/%40media/prefers-reduced-motion
🏁 Script executed:
Repository: Devasy/splitwiser
Length of output: 768
🏁 Script executed:
Repository: Devasy/splitwiser
Length of output: 2251
Honor
prefers-reduced-motionto improve accessibility for motion-sensitive users.This component is animation-heavy with entrance animations and confetti effects. Please respect user reduced-motion settings by gating confetti and disabling animations when
prefers-reduced-motionis active.Example approach using `useReducedMotion()` from framer-motion v11
Also consider adding reduced-motion support to the
Confetticomponent itself so it respects user preferences when used elsewhere.🤖 Prompt for AI Agents