diff --git a/package-lock.json b/package-lock.json index b89773e..e17d3a2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "dependencies": { "@astrojs/react": "3.6.0", "@floating-ui/react": "0.26.0", + "@formkit/auto-animate": "0.8.2", "@radix-ui/react-dropdown-menu": "2.0.6", "@radix-ui/react-tooltip": "1.0.7", "@types/howler": "2.2.10", @@ -3598,6 +3599,11 @@ "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.1.6.tgz", "integrity": "sha512-OfX7E2oUDYxtBvsuS4e/jSn4Q9Qb6DzgeYtsAdkPZ47znpoNsMgZw0+tVijiv3uGNR6dgNlty6r9rzIzHjtd/A==" }, + "node_modules/@formkit/auto-animate": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/@formkit/auto-animate/-/auto-animate-0.8.2.tgz", + "integrity": "sha512-SwPWfeRa5veb1hOIBMdzI+73te5puUBHmqqaF1Bu7FjvxlYSz/kJcZKSa9Cg60zL0uRNeJL2SbRxV6Jp6Q1nFQ==" + }, "node_modules/@humanwhocodes/config-array": { "version": "0.11.11", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.11.tgz", diff --git a/package.json b/package.json index 48f2ac8..e06dfcf 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,7 @@ "dependencies": { "@astrojs/react": "3.6.0", "@floating-ui/react": "0.26.0", + "@formkit/auto-animate": "0.8.2", "@radix-ui/react-dropdown-menu": "2.0.6", "@radix-ui/react-tooltip": "1.0.7", "@types/howler": "2.2.10", diff --git a/src/components/toolbox/countdown-timer/countdown-timer.tsx b/src/components/toolbox/countdown-timer/countdown-timer.tsx index 5c6c71c..30629ae 100644 --- a/src/components/toolbox/countdown-timer/countdown-timer.tsx +++ b/src/components/toolbox/countdown-timer/countdown-timer.tsx @@ -1,3 +1,5 @@ +import { useAutoAnimate } from '@formkit/auto-animate/react'; + import { Modal } from '@/components/modal'; import { Form } from './form'; @@ -11,11 +13,13 @@ interface TimerProps { } export function CountdownTimer({ onClose, show }: TimerProps) { + const [containerRef, enableAnimations] = useAutoAnimate(); + return (

Countdown Timer

-
- + + ); } diff --git a/src/components/toolbox/countdown-timer/form/form.tsx b/src/components/toolbox/countdown-timer/form/form.tsx index 5c4cec7..275c915 100644 --- a/src/components/toolbox/countdown-timer/form/form.tsx +++ b/src/components/toolbox/countdown-timer/form/form.tsx @@ -7,7 +7,11 @@ import { waitUntil } from '@/helpers/wait'; import styles from './form.module.css'; -export function Form() { +interface FormProps { + enableAnimations: (enabled: boolean) => void; +} + +export function Form({ enableAnimations }: FormProps) { const [name, setName] = useState(''); const [hours, setHours] = useState(0); const [minutes, setMinutes] = useState(10); @@ -25,6 +29,8 @@ export function Form() { if (totalSeconds === 0) return; + enableAnimations(false); + const id = add({ name, total: totalSeconds, @@ -37,6 +43,8 @@ export function Form() { document .getElementById(`timer-${id}`) ?.scrollIntoView({ behavior: 'smooth' }); + + enableAnimations(true); }; return ( diff --git a/src/components/toolbox/countdown-timer/timers/timer/timer.tsx b/src/components/toolbox/countdown-timer/timers/timer/timer.tsx index c73a6de..d22770e 100644 --- a/src/components/toolbox/countdown-timer/timers/timer/timer.tsx +++ b/src/components/toolbox/countdown-timer/timers/timer/timer.tsx @@ -7,6 +7,7 @@ import { } from 'react-icons/io5/index'; import { ReverseTimer } from './reverse-timer'; +import { Toolbar } from './toolbar'; import { useCountdownTimers } from '@/stores/countdown-timers'; import { useAlarm } from '@/hooks/use-alarm'; @@ -17,17 +18,18 @@ import { cn } from '@/helpers/styles'; import styles from './timer.module.css'; interface TimerProps { + enableAnimations: (enabled: boolean) => void; id: string; } -export function Timer({ id }: TimerProps) { +export function Timer({ enableAnimations, id }: TimerProps) { const intervalRef = useRef | null>(null); const lastActiveTimeRef = useRef(null); const lastStateRef = useRef<{ spent: number; total: number } | null>(null); const [isRunning, setIsRunning] = useState(false); - const { name, spent, total } = useCountdownTimers(state => + const { first, last, name, spent, total } = useCountdownTimers(state => state.getTimer(id), ) || { name: '', spent: 0, total: 0 }; @@ -75,10 +77,14 @@ export function Timer({ id }: TimerProps) { const handleDelete = () => { if (isRunning) return showSnackbar('Please first stop the timer.'); + enableAnimations(false); + setIsDeleting(true); setSnapshot({ spent, total }); deleteTimer(id); + + setTimeout(() => enableAnimations(true), 100); }; useEffect(() => { @@ -149,6 +155,8 @@ export function Timer({ id }: TimerProps) { + +
diff --git a/src/components/toolbox/countdown-timer/timers/timer/toolbar/index.ts b/src/components/toolbox/countdown-timer/timers/timer/toolbar/index.ts new file mode 100644 index 0000000..dc5abb1 --- /dev/null +++ b/src/components/toolbox/countdown-timer/timers/timer/toolbar/index.ts @@ -0,0 +1 @@ +export { Toolbar } from './toolbar'; diff --git a/src/components/toolbox/countdown-timer/timers/timer/toolbar/toolbar.module.css b/src/components/toolbox/countdown-timer/timers/timer/toolbar/toolbar.module.css new file mode 100644 index 0000000..473234c --- /dev/null +++ b/src/components/toolbox/countdown-timer/timers/timer/toolbar/toolbar.module.css @@ -0,0 +1,37 @@ +.toolbar { + position: absolute; + top: 12px; + right: 12px; + display: flex; + column-gap: 4px; + align-items: center; + height: 30px; + padding: 4px; + background-color: var(--color-neutral-50); + border: 1px solid var(--color-neutral-200); + border-radius: 4px; + + & button { + display: flex; + align-items: center; + justify-content: center; + height: 100%; + aspect-ratio: 1 / 1; + font-size: var(--font-xsm); + color: var(--color-foreground-subtle); + cursor: pointer; + background-color: transparent; + border: none; + transition: 0.2s; + + &:disabled { + cursor: not-allowed; + opacity: 0.2; + } + + &:not(:disabled):hover { + color: var(--color-foreground); + background-color: var(--color-neutral-100); + } + } +} diff --git a/src/components/toolbox/countdown-timer/timers/timer/toolbar/toolbar.tsx b/src/components/toolbox/countdown-timer/timers/timer/toolbar/toolbar.tsx new file mode 100644 index 0000000..aec9535 --- /dev/null +++ b/src/components/toolbox/countdown-timer/timers/timer/toolbar/toolbar.tsx @@ -0,0 +1,39 @@ +import { IoIosArrowDown, IoIosArrowUp } from 'react-icons/io/index'; + +import { useCountdownTimers } from '@/stores/countdown-timers'; + +import styles from './toolbar.module.css'; + +interface ToolbarProps { + first: boolean; + id: string; + last: boolean; +} + +export function Toolbar({ first, id, last }: ToolbarProps) { + const moveUp = useCountdownTimers(state => state.moveUp); + const moveDown = useCountdownTimers(state => state.moveDown); + + return ( +
+ + +
+ ); +} diff --git a/src/components/toolbox/countdown-timer/timers/timers.tsx b/src/components/toolbox/countdown-timer/timers/timers.tsx index daa35b7..52abbc9 100644 --- a/src/components/toolbox/countdown-timer/timers/timers.tsx +++ b/src/components/toolbox/countdown-timer/timers/timers.tsx @@ -1,4 +1,4 @@ -import { useMemo } from 'react'; +import { useMemo, forwardRef } from 'react'; import { Timer } from './timer'; import { Notice } from './notice'; @@ -7,7 +7,14 @@ import { useCountdownTimers } from '@/stores/countdown-timers'; import styles from './timers.module.css'; -export function Timers() { +interface TimersProps { + enableAnimations: (enabled: boolean) => void; +} + +export const Timers = forwardRef(function Timers( + { enableAnimations }: TimersProps, + ref: React.ForwardedRef, +) { const timers = useCountdownTimers(state => state.timers); const spent = useCountdownTimers(state => state.spent()); const total = useCountdownTimers(state => state.total()); @@ -30,13 +37,19 @@ export function Timers() { )} - {timers.map(timer => ( - - ))} +
+ {timers.map(timer => ( + + ))} +
) : null} ); -} +}); diff --git a/src/stores/countdown-timers/index.ts b/src/stores/countdown-timers/index.ts index dca63d1..977a14c 100644 --- a/src/stores/countdown-timers/index.ts +++ b/src/stores/countdown-timers/index.ts @@ -18,7 +18,9 @@ interface State { interface Actions { add: (timer: { name: string; total: number }) => string; delete: (id: string) => void; - getTimer: (id: string) => Timer; + getTimer: (id: string) => Timer & { first: boolean; last: boolean }; + moveDown: (id: string) => void; + moveUp: (id: string) => void; rename: (id: string, newName: string) => void; reset: (id: string) => void; tick: (id: string, amount?: number) => void; @@ -52,7 +54,53 @@ export const useCountdownTimers = create()( }, getTimer(id) { - return get().timers.filter(timer => timer.id === id)[0]; + const timers = get().timers; + const timer = timers.filter(timer => timer.id === id)[0]; + const index = timers.indexOf(timer); + + return { + ...timer, + first: index === 0, + last: index === timers.length - 1, + }; + }, + + moveDown(id) { + set(state => { + const index = state.timers.findIndex(timer => timer.id === id); + + if (index < state.timers.length - 1) { + const newTimers = [...state.timers]; + + [newTimers[index + 1], newTimers[index]] = [ + newTimers[index], + newTimers[index + 1], + ]; + + return { timers: newTimers }; + } + + return state; + }); + }, + + moveUp(id) { + set(state => { + const index = state.timers.findIndex(timer => timer.id === id); + + if (index > 0) { + const newTimers = [...state.timers]; + + [newTimers[index - 1], newTimers[index]] = [ + newTimers[index], + newTimers[index - 1], + ]; + + return { timers: newTimers }; + } + + return state; + }); }, rename(id, newName) {