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 { Form } from './form';
|
||||
|
||||
interface TimerProps {
|
||||
onClose: () => void;
|
||||
show: boolean;
|
||||
@ -9,6 +11,7 @@ export function CountdownTimer({ onClose, show }: TimerProps) {
|
||||
return (
|
||||
<Modal show={show} onClose={onClose}>
|
||||
<h1>Hello World</h1>
|
||||
<Form />
|
||||
</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