From c27291441625eb6528b28f55af3f88e1debd8a55 Mon Sep 17 00:00:00 2001 From: MAZE Date: Sun, 16 Jun 2024 19:00:38 +0430 Subject: [PATCH] feat: add basic form --- .../countdown-timer/countdown-timer.tsx | 3 + .../form/field/field.module.css | 27 +++++ .../countdown-timer/form/field/field.tsx | 51 +++++++++ .../countdown-timer/form/field/index.ts | 1 + .../countdown-timer/form/form.module.css | 28 +++++ .../toolbox/countdown-timer/form/form.tsx | 97 ++++++++++++++++ .../toolbox/countdown-timer/form/index.ts | 1 + src/stores/countdown-timers/index.ts | 107 ++++++++++++++++++ 8 files changed, 315 insertions(+) create mode 100644 src/components/toolbox/countdown-timer/form/field/field.module.css create mode 100644 src/components/toolbox/countdown-timer/form/field/field.tsx create mode 100644 src/components/toolbox/countdown-timer/form/field/index.ts create mode 100644 src/components/toolbox/countdown-timer/form/form.module.css create mode 100644 src/components/toolbox/countdown-timer/form/form.tsx create mode 100644 src/components/toolbox/countdown-timer/form/index.ts create mode 100644 src/stores/countdown-timers/index.ts diff --git a/src/components/toolbox/countdown-timer/countdown-timer.tsx b/src/components/toolbox/countdown-timer/countdown-timer.tsx index 613ca15..378066d 100644 --- a/src/components/toolbox/countdown-timer/countdown-timer.tsx +++ b/src/components/toolbox/countdown-timer/countdown-timer.tsx @@ -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 (

Hello World

+
); } diff --git a/src/components/toolbox/countdown-timer/form/field/field.module.css b/src/components/toolbox/countdown-timer/form/field/field.module.css new file mode 100644 index 0000000..585811f --- /dev/null +++ b/src/components/toolbox/countdown-timer/form/field/field.module.css @@ -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; + } +} diff --git a/src/components/toolbox/countdown-timer/form/field/field.tsx b/src/components/toolbox/countdown-timer/form/field/field.tsx new file mode 100644 index 0000000..6b057a8 --- /dev/null +++ b/src/components/toolbox/countdown-timer/form/field/field.tsx @@ -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 ( +
+ + + {type === 'text' && ( + onChange(e.target.value)} + /> + )} + + {type === 'select' && ( + + )} +
+ ); +} diff --git a/src/components/toolbox/countdown-timer/form/field/index.ts b/src/components/toolbox/countdown-timer/form/field/index.ts new file mode 100644 index 0000000..497b3a7 --- /dev/null +++ b/src/components/toolbox/countdown-timer/form/field/index.ts @@ -0,0 +1 @@ +export { Field } from './field'; diff --git a/src/components/toolbox/countdown-timer/form/form.module.css b/src/components/toolbox/countdown-timer/form/form.module.css new file mode 100644 index 0000000..b6a60ee --- /dev/null +++ b/src/components/toolbox/countdown-timer/form/form.module.css @@ -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; +} diff --git a/src/components/toolbox/countdown-timer/form/form.tsx b/src/components/toolbox/countdown-timer/form/form.tsx new file mode 100644 index 0000000..066df86 --- /dev/null +++ b/src/components/toolbox/countdown-timer/form/form.tsx @@ -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) => { + e.preventDefault(); + + if (totalSeconds === 0) return; + + add({ + name, + total: totalSeconds, + }); + + setName(''); + }; + + return ( + + setName(value as string)} + /> + +
+ setHours(value as number)} + > + {Array(13) + .fill(null) + .map((_, index) => ( + + ))} + + + setMinutes(value as number)} + > + {Array(60) + .fill(null) + .map((_, index) => ( + + ))} + + + setSeconds(value as number)} + > + {Array(60) + .fill(null) + .map((_, index) => ( + + ))} + +
+ + + + ); +} diff --git a/src/components/toolbox/countdown-timer/form/index.ts b/src/components/toolbox/countdown-timer/form/index.ts new file mode 100644 index 0000000..9398c9e --- /dev/null +++ b/src/components/toolbox/countdown-timer/form/index.ts @@ -0,0 +1 @@ +export { Form } from './form'; diff --git a/src/stores/countdown-timers/index.ts b/src/stores/countdown-timers/index.ts new file mode 100644 index 0000000..0f44edf --- /dev/null +++ b/src/stores/countdown-timers/index.ts @@ -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; + 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()( + 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, + }, + ), +);