mirror of
https://github.com/remvze/moodist.git
synced 2025-09-29 15:30:49 -04:00
feat: add basic form
This commit is contained in:
parent
d73b2bc1ff
commit
c272914416
@ -1,5 +1,7 @@
|
|||||||
import { Modal } from '@/components/modal';
|
import { Modal } from '@/components/modal';
|
||||||
|
|
||||||
|
import { Form } from './form';
|
||||||
|
|
||||||
interface TimerProps {
|
interface TimerProps {
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
show: boolean;
|
show: boolean;
|
||||||
@ -9,6 +11,7 @@ export function CountdownTimer({ onClose, show }: TimerProps) {
|
|||||||
return (
|
return (
|
||||||
<Modal show={show} onClose={onClose}>
|
<Modal show={show} onClose={onClose}>
|
||||||
<h1>Hello World</h1>
|
<h1>Hello World</h1>
|
||||||
|
<Form />
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,27 @@
|
|||||||
|
.field {
|
||||||
|
flex-grow: 1;
|
||||||
|
|
||||||
|
& .label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
font-size: var(--font-sm);
|
||||||
|
font-weight: 500;
|
||||||
|
|
||||||
|
& .optional {
|
||||||
|
font-weight: 400;
|
||||||
|
color: var(--color-foreground-subtle);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
& .input {
|
||||||
|
width: 100%;
|
||||||
|
min-width: 0;
|
||||||
|
height: 40px;
|
||||||
|
padding: 0 16px;
|
||||||
|
color: var(--color-foreground);
|
||||||
|
background-color: var(--color-neutral-50);
|
||||||
|
border: 1px solid var(--color-neutral-200);
|
||||||
|
border-radius: 4px;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
}
|
51
src/components/toolbox/countdown-timer/form/field/field.tsx
Normal file
51
src/components/toolbox/countdown-timer/form/field/field.tsx
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
import styles from './field.module.css';
|
||||||
|
|
||||||
|
interface FieldProps {
|
||||||
|
children?: React.ReactNode;
|
||||||
|
label: string;
|
||||||
|
onChange: (value: string | number) => void;
|
||||||
|
optional?: boolean;
|
||||||
|
type: 'text' | 'select';
|
||||||
|
value: string | number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Field({
|
||||||
|
children,
|
||||||
|
label,
|
||||||
|
onChange,
|
||||||
|
optional,
|
||||||
|
type,
|
||||||
|
value,
|
||||||
|
}: FieldProps) {
|
||||||
|
return (
|
||||||
|
<div className={styles.field}>
|
||||||
|
<label className={styles.label} htmlFor={label.toLowerCase()}>
|
||||||
|
{label}{' '}
|
||||||
|
{optional && <span className={styles.optional}>(optional)</span>}
|
||||||
|
</label>
|
||||||
|
|
||||||
|
{type === 'text' && (
|
||||||
|
<input
|
||||||
|
autoComplete="off"
|
||||||
|
className={styles.input}
|
||||||
|
id={label.toLowerCase()}
|
||||||
|
type="text"
|
||||||
|
value={value}
|
||||||
|
onChange={e => onChange(e.target.value)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{type === 'select' && (
|
||||||
|
<select
|
||||||
|
autoComplete="off"
|
||||||
|
className={styles.input}
|
||||||
|
id={label.toLowerCase()}
|
||||||
|
value={value}
|
||||||
|
onChange={e => onChange(parseInt(e.target.value))}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</select>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
@ -0,0 +1 @@
|
|||||||
|
export { Field } from './field';
|
28
src/components/toolbox/countdown-timer/form/form.module.css
Normal file
28
src/components/toolbox/countdown-timer/form/form.module.css
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
.form {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
row-gap: 28px;
|
||||||
|
|
||||||
|
& .button {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 100%;
|
||||||
|
height: 45px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--color-neutral-50);
|
||||||
|
cursor: pointer;
|
||||||
|
background-color: var(--color-neutral-950);
|
||||||
|
border: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
outline: none;
|
||||||
|
box-shadow: inset 0 -3px 0 var(--color-neutral-700);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.timeFields {
|
||||||
|
display: flex;
|
||||||
|
column-gap: 12px;
|
||||||
|
align-items: flex-end;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
97
src/components/toolbox/countdown-timer/form/form.tsx
Normal file
97
src/components/toolbox/countdown-timer/form/form.tsx
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
import { useState, useMemo } from 'react';
|
||||||
|
|
||||||
|
import { Field } from './field';
|
||||||
|
|
||||||
|
import { useCountdownTimers } from '@/stores/countdown-timers';
|
||||||
|
|
||||||
|
import styles from './form.module.css';
|
||||||
|
|
||||||
|
export function Form() {
|
||||||
|
const [name, setName] = useState('');
|
||||||
|
const [hours, setHours] = useState(0);
|
||||||
|
const [minutes, setMinutes] = useState(10);
|
||||||
|
const [seconds, setSeconds] = useState(0);
|
||||||
|
|
||||||
|
const totalSeconds = useMemo(
|
||||||
|
() => hours * 60 * 60 + minutes * 60 + seconds,
|
||||||
|
[hours, minutes, seconds],
|
||||||
|
);
|
||||||
|
|
||||||
|
const add = useCountdownTimers(state => state.add);
|
||||||
|
|
||||||
|
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
if (totalSeconds === 0) return;
|
||||||
|
|
||||||
|
add({
|
||||||
|
name,
|
||||||
|
total: totalSeconds,
|
||||||
|
});
|
||||||
|
|
||||||
|
setName('');
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form className={styles.form} onSubmit={handleSubmit}>
|
||||||
|
<Field
|
||||||
|
label="Timer Name"
|
||||||
|
optional
|
||||||
|
type="text"
|
||||||
|
value={name}
|
||||||
|
onChange={value => setName(value as string)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className={styles.timeFields}>
|
||||||
|
<Field
|
||||||
|
label="Hours"
|
||||||
|
type="select"
|
||||||
|
value={hours}
|
||||||
|
onChange={value => setHours(value as number)}
|
||||||
|
>
|
||||||
|
{Array(13)
|
||||||
|
.fill(null)
|
||||||
|
.map((_, index) => (
|
||||||
|
<option key={`hour-${index}`} value={index}>
|
||||||
|
{index}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</Field>
|
||||||
|
|
||||||
|
<Field
|
||||||
|
label="Minutes"
|
||||||
|
type="select"
|
||||||
|
value={minutes}
|
||||||
|
onChange={value => setMinutes(value as number)}
|
||||||
|
>
|
||||||
|
{Array(60)
|
||||||
|
.fill(null)
|
||||||
|
.map((_, index) => (
|
||||||
|
<option key={`minutes-${index}`} value={index}>
|
||||||
|
{index}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</Field>
|
||||||
|
|
||||||
|
<Field
|
||||||
|
label="Seconds"
|
||||||
|
type="select"
|
||||||
|
value={seconds}
|
||||||
|
onChange={value => setSeconds(value as number)}
|
||||||
|
>
|
||||||
|
{Array(60)
|
||||||
|
.fill(null)
|
||||||
|
.map((_, index) => (
|
||||||
|
<option key={`seconds-${index}`} value={index}>
|
||||||
|
{index}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</Field>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button className={styles.button} type="submit">
|
||||||
|
Add Timer
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
}
|
1
src/components/toolbox/countdown-timer/form/index.ts
Normal file
1
src/components/toolbox/countdown-timer/form/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export { Form } from './form';
|
107
src/stores/countdown-timers/index.ts
Normal file
107
src/stores/countdown-timers/index.ts
Normal file
@ -0,0 +1,107 @@
|
|||||||
|
import { v4 as uuid } from 'uuid';
|
||||||
|
import { create } from 'zustand';
|
||||||
|
import { createJSONStorage, persist } from 'zustand/middleware';
|
||||||
|
|
||||||
|
interface Timer {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
spent: number;
|
||||||
|
total: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface State {
|
||||||
|
spent: () => number;
|
||||||
|
timers: Array<Timer>;
|
||||||
|
total: () => number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Actions {
|
||||||
|
add: (timer: { name: string; total: number }) => void;
|
||||||
|
delete: (id: string) => void;
|
||||||
|
getTimer: (id: string) => Timer;
|
||||||
|
rename: (id: string, newName: string) => void;
|
||||||
|
reset: (id: string) => void;
|
||||||
|
tick: (id: string, amount?: number) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useCountdownTimers = create<State & Actions>()(
|
||||||
|
persist(
|
||||||
|
(set, get) => ({
|
||||||
|
add({ name, total }) {
|
||||||
|
set(state => ({
|
||||||
|
timers: [
|
||||||
|
{
|
||||||
|
id: uuid(),
|
||||||
|
name,
|
||||||
|
spent: 0,
|
||||||
|
total,
|
||||||
|
},
|
||||||
|
...state.timers,
|
||||||
|
],
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
|
||||||
|
delete(id) {
|
||||||
|
set(state => ({
|
||||||
|
timers: state.timers.filter(timer => timer.id !== id),
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
|
||||||
|
getTimer(id) {
|
||||||
|
return get().timers.filter(timer => timer.id === id)[0];
|
||||||
|
},
|
||||||
|
|
||||||
|
rename(id, newName) {
|
||||||
|
set(state => ({
|
||||||
|
timers: state.timers.map(timer => {
|
||||||
|
if (timer.id !== id) return timer;
|
||||||
|
|
||||||
|
return { ...timer, name: newName };
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
|
||||||
|
reset(id) {
|
||||||
|
set(state => ({
|
||||||
|
timers: state.timers.map(timer => {
|
||||||
|
if (timer.id !== id) return timer;
|
||||||
|
|
||||||
|
return { ...timer, spent: 0 };
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
|
||||||
|
spent() {
|
||||||
|
return get().timers.reduce((prev, curr) => prev + curr.spent, 0);
|
||||||
|
},
|
||||||
|
|
||||||
|
tick(id, amount = 1) {
|
||||||
|
set(state => ({
|
||||||
|
timers: state.timers.map(timer => {
|
||||||
|
if (timer.id !== id) return timer;
|
||||||
|
|
||||||
|
const updatedSpent =
|
||||||
|
timer.spent + amount > timer.total
|
||||||
|
? timer.total
|
||||||
|
: timer.spent + amount;
|
||||||
|
|
||||||
|
return { ...timer, spent: updatedSpent };
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
|
||||||
|
timers: [],
|
||||||
|
|
||||||
|
total() {
|
||||||
|
return get().timers.reduce((prev, curr) => prev + curr.total, 0);
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
name: 'moodist-countdown-timers',
|
||||||
|
partialize: state => ({ timers: state.timers }),
|
||||||
|
skipHydration: true,
|
||||||
|
storage: createJSONStorage(() => localStorage),
|
||||||
|
version: 0,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
Loading…
x
Reference in New Issue
Block a user