mirror of
https://github.com/remvze/moodist.git
synced 2025-12-18 17:34:17 +00:00
feat: add move up and down functionality
This commit is contained in:
parent
d356d77aa9
commit
3e11fb6123
10 changed files with 178 additions and 13 deletions
6
package-lock.json
generated
6
package-lock.json
generated
|
|
@ -10,6 +10,7 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@astrojs/react": "3.6.0",
|
"@astrojs/react": "3.6.0",
|
||||||
"@floating-ui/react": "0.26.0",
|
"@floating-ui/react": "0.26.0",
|
||||||
|
"@formkit/auto-animate": "0.8.2",
|
||||||
"@radix-ui/react-dropdown-menu": "2.0.6",
|
"@radix-ui/react-dropdown-menu": "2.0.6",
|
||||||
"@radix-ui/react-tooltip": "1.0.7",
|
"@radix-ui/react-tooltip": "1.0.7",
|
||||||
"@types/howler": "2.2.10",
|
"@types/howler": "2.2.10",
|
||||||
|
|
@ -3598,6 +3599,11 @@
|
||||||
"resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.1.6.tgz",
|
"resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.1.6.tgz",
|
||||||
"integrity": "sha512-OfX7E2oUDYxtBvsuS4e/jSn4Q9Qb6DzgeYtsAdkPZ47znpoNsMgZw0+tVijiv3uGNR6dgNlty6r9rzIzHjtd/A=="
|
"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": {
|
"node_modules/@humanwhocodes/config-array": {
|
||||||
"version": "0.11.11",
|
"version": "0.11.11",
|
||||||
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.11.tgz",
|
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.11.tgz",
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,7 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@astrojs/react": "3.6.0",
|
"@astrojs/react": "3.6.0",
|
||||||
"@floating-ui/react": "0.26.0",
|
"@floating-ui/react": "0.26.0",
|
||||||
|
"@formkit/auto-animate": "0.8.2",
|
||||||
"@radix-ui/react-dropdown-menu": "2.0.6",
|
"@radix-ui/react-dropdown-menu": "2.0.6",
|
||||||
"@radix-ui/react-tooltip": "1.0.7",
|
"@radix-ui/react-tooltip": "1.0.7",
|
||||||
"@types/howler": "2.2.10",
|
"@types/howler": "2.2.10",
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
import { useAutoAnimate } from '@formkit/auto-animate/react';
|
||||||
|
|
||||||
import { Modal } from '@/components/modal';
|
import { Modal } from '@/components/modal';
|
||||||
|
|
||||||
import { Form } from './form';
|
import { Form } from './form';
|
||||||
|
|
@ -11,11 +13,13 @@ interface TimerProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function CountdownTimer({ onClose, show }: TimerProps) {
|
export function CountdownTimer({ onClose, show }: TimerProps) {
|
||||||
|
const [containerRef, enableAnimations] = useAutoAnimate<HTMLDivElement>();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal persist show={show} onClose={onClose}>
|
<Modal persist show={show} onClose={onClose}>
|
||||||
<h2 className={styles.title}>Countdown Timer</h2>
|
<h2 className={styles.title}>Countdown Timer</h2>
|
||||||
<Form />
|
<Form enableAnimations={enableAnimations} />
|
||||||
<Timers />
|
<Timers enableAnimations={enableAnimations} ref={containerRef} />
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,11 @@ import { waitUntil } from '@/helpers/wait';
|
||||||
|
|
||||||
import styles from './form.module.css';
|
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 [name, setName] = useState('');
|
||||||
const [hours, setHours] = useState(0);
|
const [hours, setHours] = useState(0);
|
||||||
const [minutes, setMinutes] = useState(10);
|
const [minutes, setMinutes] = useState(10);
|
||||||
|
|
@ -25,6 +29,8 @@ export function Form() {
|
||||||
|
|
||||||
if (totalSeconds === 0) return;
|
if (totalSeconds === 0) return;
|
||||||
|
|
||||||
|
enableAnimations(false);
|
||||||
|
|
||||||
const id = add({
|
const id = add({
|
||||||
name,
|
name,
|
||||||
total: totalSeconds,
|
total: totalSeconds,
|
||||||
|
|
@ -37,6 +43,8 @@ export function Form() {
|
||||||
document
|
document
|
||||||
.getElementById(`timer-${id}`)
|
.getElementById(`timer-${id}`)
|
||||||
?.scrollIntoView({ behavior: 'smooth' });
|
?.scrollIntoView({ behavior: 'smooth' });
|
||||||
|
|
||||||
|
enableAnimations(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import {
|
||||||
} from 'react-icons/io5/index';
|
} from 'react-icons/io5/index';
|
||||||
|
|
||||||
import { ReverseTimer } from './reverse-timer';
|
import { ReverseTimer } from './reverse-timer';
|
||||||
|
import { Toolbar } from './toolbar';
|
||||||
|
|
||||||
import { useCountdownTimers } from '@/stores/countdown-timers';
|
import { useCountdownTimers } from '@/stores/countdown-timers';
|
||||||
import { useAlarm } from '@/hooks/use-alarm';
|
import { useAlarm } from '@/hooks/use-alarm';
|
||||||
|
|
@ -17,17 +18,18 @@ import { cn } from '@/helpers/styles';
|
||||||
import styles from './timer.module.css';
|
import styles from './timer.module.css';
|
||||||
|
|
||||||
interface TimerProps {
|
interface TimerProps {
|
||||||
|
enableAnimations: (enabled: boolean) => void;
|
||||||
id: string;
|
id: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Timer({ id }: TimerProps) {
|
export function Timer({ enableAnimations, id }: TimerProps) {
|
||||||
const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
||||||
const lastActiveTimeRef = useRef<number | null>(null);
|
const lastActiveTimeRef = useRef<number | null>(null);
|
||||||
const lastStateRef = useRef<{ spent: number; total: number } | null>(null);
|
const lastStateRef = useRef<{ spent: number; total: number } | null>(null);
|
||||||
|
|
||||||
const [isRunning, setIsRunning] = useState(false);
|
const [isRunning, setIsRunning] = useState(false);
|
||||||
|
|
||||||
const { name, spent, total } = useCountdownTimers(state =>
|
const { first, last, name, spent, total } = useCountdownTimers(state =>
|
||||||
state.getTimer(id),
|
state.getTimer(id),
|
||||||
) || { name: '', spent: 0, total: 0 };
|
) || { name: '', spent: 0, total: 0 };
|
||||||
|
|
||||||
|
|
@ -75,10 +77,14 @@ export function Timer({ id }: TimerProps) {
|
||||||
const handleDelete = () => {
|
const handleDelete = () => {
|
||||||
if (isRunning) return showSnackbar('Please first stop the timer.');
|
if (isRunning) return showSnackbar('Please first stop the timer.');
|
||||||
|
|
||||||
|
enableAnimations(false);
|
||||||
|
|
||||||
setIsDeleting(true);
|
setIsDeleting(true);
|
||||||
setSnapshot({ spent, total });
|
setSnapshot({ spent, total });
|
||||||
|
|
||||||
deleteTimer(id);
|
deleteTimer(id);
|
||||||
|
|
||||||
|
setTimeout(() => enableAnimations(true), 100);
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -149,6 +155,8 @@ export function Timer({ id }: TimerProps) {
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
|
<Toolbar first={first} id={id} last={last} />
|
||||||
|
|
||||||
<ReverseTimer spent={spent} />
|
<ReverseTimer spent={spent} />
|
||||||
|
|
||||||
<div className={styles.left}>
|
<div className={styles.left}>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
export { Toolbar } from './toolbar';
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { useMemo } from 'react';
|
import { useMemo, forwardRef } from 'react';
|
||||||
|
|
||||||
import { Timer } from './timer';
|
import { Timer } from './timer';
|
||||||
import { Notice } from './notice';
|
import { Notice } from './notice';
|
||||||
|
|
@ -7,7 +7,14 @@ import { useCountdownTimers } from '@/stores/countdown-timers';
|
||||||
|
|
||||||
import styles from './timers.module.css';
|
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 timers = useCountdownTimers(state => state.timers);
|
||||||
const spent = useCountdownTimers(state => state.spent());
|
const spent = useCountdownTimers(state => state.spent());
|
||||||
const total = useCountdownTimers(state => state.total());
|
const total = useCountdownTimers(state => state.total());
|
||||||
|
|
@ -30,13 +37,19 @@ export function Timers() {
|
||||||
)}
|
)}
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
|
<div ref={ref}>
|
||||||
{timers.map(timer => (
|
{timers.map(timer => (
|
||||||
<Timer id={timer.id} key={timer.id} />
|
<Timer
|
||||||
|
enableAnimations={enableAnimations}
|
||||||
|
id={timer.id}
|
||||||
|
key={timer.id}
|
||||||
|
/>
|
||||||
))}
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
<Notice />
|
<Notice />
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
});
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,9 @@ interface State {
|
||||||
interface Actions {
|
interface Actions {
|
||||||
add: (timer: { name: string; total: number }) => string;
|
add: (timer: { name: string; total: number }) => string;
|
||||||
delete: (id: string) => void;
|
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;
|
rename: (id: string, newName: string) => void;
|
||||||
reset: (id: string) => void;
|
reset: (id: string) => void;
|
||||||
tick: (id: string, amount?: number) => void;
|
tick: (id: string, amount?: number) => void;
|
||||||
|
|
@ -52,7 +54,53 @@ export const useCountdownTimers = create<State & Actions>()(
|
||||||
},
|
},
|
||||||
|
|
||||||
getTimer(id) {
|
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) {
|
rename(id, newName) {
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue