feat: add move up and down functionality

This commit is contained in:
MAZE 2024-06-25 19:56:04 +04:30
parent d356d77aa9
commit 3e11fb6123
10 changed files with 178 additions and 13 deletions

6
package-lock.json generated
View file

@ -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",

View file

@ -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",

View file

@ -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<HTMLDivElement>();
return (
<Modal persist show={show} onClose={onClose}>
<h2 className={styles.title}>Countdown Timer</h2>
<Form />
<Timers />
<Form enableAnimations={enableAnimations} />
<Timers enableAnimations={enableAnimations} ref={containerRef} />
</Modal>
);
}

View file

@ -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 (

View file

@ -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<ReturnType<typeof setInterval> | null>(null);
const lastActiveTimeRef = useRef<number | null>(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) {
</div>
</header>
<Toolbar first={first} id={id} last={last} />
<ReverseTimer spent={spent} />
<div className={styles.left}>

View file

@ -0,0 +1 @@
export { Toolbar } from './toolbar';

View file

@ -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);
}
}
}

View file

@ -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 (
<div className={styles.toolbar}>
<button
disabled={first}
onClick={e => {
e.stopPropagation();
moveUp(id);
}}
>
<IoIosArrowUp />
</button>
<button
disabled={last}
onClick={e => {
e.stopPropagation();
moveDown(id);
}}
>
<IoIosArrowDown />
</button>
</div>
);
}

View file

@ -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<HTMLDivElement>,
) {
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() {
)}
</header>
{timers.map(timer => (
<Timer id={timer.id} key={timer.id} />
))}
<div ref={ref}>
{timers.map(timer => (
<Timer
enableAnimations={enableAnimations}
id={timer.id}
key={timer.id}
/>
))}
</div>
<Notice />
</div>
) : null}
</div>
);
}
});

View file

@ -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<State & Actions>()(
},
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) {