From 2484e01273cf5e7ef5b44395cab26095891118fd Mon Sep 17 00:00:00 2001 From: MAZE Date: Wed, 28 Feb 2024 19:59:39 +0330 Subject: [PATCH] feat: add custom presets --- src/components/menu/items/index.ts | 1 + src/components/menu/items/presets.tsx | 11 ++++ src/components/menu/menu.tsx | 6 ++ src/components/modals/presets/index.ts | 1 + src/components/modals/presets/list/index.ts | 1 + .../modals/presets/list/list.module.css | 61 +++++++++++++++++++ src/components/modals/presets/list/list.tsx | 53 ++++++++++++++++ src/components/modals/presets/new/index.ts | 1 + .../modals/presets/new/new.module.css | 61 +++++++++++++++++++ src/components/modals/presets/new/new.tsx | 59 ++++++++++++++++++ .../modals/presets/presets.module.css | 12 ++++ src/components/modals/presets/presets.tsx | 21 +++++++ src/store/index.ts | 1 + src/store/preset/index.ts | 33 ++++++++++ src/store/sound/sound.actions.ts | 8 ++- 15 files changed, 327 insertions(+), 3 deletions(-) create mode 100644 src/components/menu/items/presets.tsx create mode 100644 src/components/modals/presets/index.ts create mode 100644 src/components/modals/presets/list/index.ts create mode 100644 src/components/modals/presets/list/list.module.css create mode 100644 src/components/modals/presets/list/list.tsx create mode 100644 src/components/modals/presets/new/index.ts create mode 100644 src/components/modals/presets/new/new.module.css create mode 100644 src/components/modals/presets/new/new.tsx create mode 100644 src/components/modals/presets/presets.module.css create mode 100644 src/components/modals/presets/presets.tsx create mode 100644 src/store/preset/index.ts diff --git a/src/components/menu/items/index.ts b/src/components/menu/items/index.ts index f8ba87f..5b6cc80 100644 --- a/src/components/menu/items/index.ts +++ b/src/components/menu/items/index.ts @@ -4,3 +4,4 @@ export { Donate as DonateItem } from './donate'; export { Notepad as NotepadItem } from './notepad'; export { Source as SourceItem } from './source'; export { Pomodoro as PomodoroItem } from './pomodoro'; +export { Presets as PresetsItem } from './presets'; diff --git a/src/components/menu/items/presets.tsx b/src/components/menu/items/presets.tsx new file mode 100644 index 0000000..65d3f50 --- /dev/null +++ b/src/components/menu/items/presets.tsx @@ -0,0 +1,11 @@ +import { RiPlayListFill } from 'react-icons/ri/index'; + +import { Item } from '../item'; + +interface PresetsProps { + open: () => void; +} + +export function Presets({ open }: PresetsProps) { + return } label="Your Presets" onClick={open} />; +} diff --git a/src/components/menu/menu.tsx b/src/components/menu/menu.tsx index 17da617..77fc8cb 100644 --- a/src/components/menu/menu.tsx +++ b/src/components/menu/menu.tsx @@ -21,9 +21,11 @@ import { NotepadItem, SourceItem, PomodoroItem, + PresetsItem, } from './items'; import { Divider } from './divider'; import { ShareLinkModal } from '@/components/modals/share-link'; +import { PresetsModal } from '@/components/modals/presets'; import { Notepad, Pomodoro } from '@/components/toolbox'; import styles from './menu.module.css'; @@ -31,6 +33,7 @@ import styles from './menu.module.css'; export function Menu() { const [isOpen, setIsOpen] = useState(false); + const [showPresets, setShowPresets] = useState(false); const [showShareLink, setShowShareLink] = useState(false); const [showNotepad, setShowNotepad] = useState(false); const [showPomodoro, setShowPomodoro] = useState(false); @@ -86,6 +89,7 @@ export function Menu() { {...getFloatingProps()} className={styles.menu} > + setShowPresets(true)} /> setShowShareLink(true)} /> @@ -104,6 +108,8 @@ export function Menu() { onClose={() => setShowShareLink(false)} /> + setShowPresets(false)} /> + setShowNotepad(false)} /> setShowPomodoro(false)} /> diff --git a/src/components/modals/presets/index.ts b/src/components/modals/presets/index.ts new file mode 100644 index 0000000..3bf712f --- /dev/null +++ b/src/components/modals/presets/index.ts @@ -0,0 +1 @@ +export { PresetsModal } from './presets'; diff --git a/src/components/modals/presets/list/index.ts b/src/components/modals/presets/list/index.ts new file mode 100644 index 0000000..53821c4 --- /dev/null +++ b/src/components/modals/presets/list/index.ts @@ -0,0 +1 @@ +export { List } from './list'; diff --git a/src/components/modals/presets/list/list.module.css b/src/components/modals/presets/list/list.module.css new file mode 100644 index 0000000..00d90bd --- /dev/null +++ b/src/components/modals/presets/list/list.module.css @@ -0,0 +1,61 @@ +.list { + & .title { + margin-bottom: 8px; + font-weight: 500; + color: var(--color-foreground-subtle); + } + + & .empty { + font-size: var(--font-sm); + } + + & .preset { + display: flex; + column-gap: 4px; + align-items: center; + width: 100%; + height: 45px; + padding: 4px; + margin-top: 8px; + background-color: var(--color-neutral-50); + border: 1px solid var(--color-neutral-200); + border-radius: 8px; + + &:not(:last-of-type) { + margin-bottom: 8px; + } + + & input { + flex-grow: 1; + height: 100%; + padding: 0 12px; + color: var(--color-foreground); + background: transparent; + border: none; + outline: none; + } + + & button { + display: flex; + align-items: center; + justify-content: center; + height: 100%; + aspect-ratio: 1 / 1; + font-size: var(--font-sm); + font-weight: 500; + color: var(--color-foreground-subtle); + cursor: pointer; + background-color: var(--color-neutral-100); + border: none; + border-radius: 4px; + outline: none; + + &.primary { + font-size: var(--font-xsm); + color: var(--color-foreground); + background-color: var(--color-neutral-200); + border: 1px solid var(--color-neutral-300); + } + } + } +} diff --git a/src/components/modals/presets/list/list.tsx b/src/components/modals/presets/list/list.tsx new file mode 100644 index 0000000..0bb7117 --- /dev/null +++ b/src/components/modals/presets/list/list.tsx @@ -0,0 +1,53 @@ +import { FaPlay, FaRegTrashAlt } from 'react-icons/fa/index'; + +import styles from './list.module.css'; + +import { usePresetStore, useSoundStore } from '@/store'; + +interface ListProps { + close: () => void; +} + +export function List({ close }: ListProps) { + const presets = usePresetStore(state => state.presets); + const changeName = usePresetStore(state => state.changeName); + const deletePreset = usePresetStore(state => state.deletePreset); + const override = useSoundStore(state => state.override); + const play = useSoundStore(state => state.play); + + return ( +
+

+ Your Presets {presets.length > 0 && `(${presets.length})`} +

+ + {!presets.length && ( +

You don't have any presets yet.

+ )} + + {presets.map((preset, index) => ( +
+ changeName(index, e.target.value)} + /> + + +
+ ))} +
+ ); +} diff --git a/src/components/modals/presets/new/index.ts b/src/components/modals/presets/new/index.ts new file mode 100644 index 0000000..2d200c6 --- /dev/null +++ b/src/components/modals/presets/new/index.ts @@ -0,0 +1 @@ +export { New } from './new'; diff --git a/src/components/modals/presets/new/new.module.css b/src/components/modals/presets/new/new.module.css new file mode 100644 index 0000000..f73f279 --- /dev/null +++ b/src/components/modals/presets/new/new.module.css @@ -0,0 +1,61 @@ +.new { + margin-top: 16px; + + & .title { + font-weight: 500; + color: var(--color-foreground-subtle); + } + + & .form { + display: flex; + align-items: center; + width: 100%; + height: 45px; + padding: 4px; + margin-top: 8px; + background-color: var(--color-neutral-50); + border: 1px solid var(--color-neutral-200); + border-radius: 8px; + + &.disabled { + filter: blur(2px); + opacity: 0.4; + } + + & input { + flex-grow: 1; + height: 100%; + padding: 0 12px; + color: var(--color-foreground); + background: transparent; + border: none; + outline: none; + } + + & button { + display: flex; + align-items: center; + justify-content: center; + height: 100%; + padding: 0 12px; + font-size: var(--font-sm); + font-weight: 500; + color: var(--color-neutral-50); + cursor: pointer; + background-color: var(--color-neutral-950); + border: none; + border-radius: 4px; + outline: none; + + &:disabled { + cursor: not-allowed; + } + } + } + + & .noSelected { + margin-top: 8px; + font-size: var(--font-sm); + color: var(--color-foreground-subtle); + } +} diff --git a/src/components/modals/presets/new/new.tsx b/src/components/modals/presets/new/new.tsx new file mode 100644 index 0000000..462a3a9 --- /dev/null +++ b/src/components/modals/presets/new/new.tsx @@ -0,0 +1,59 @@ +import { useState, type FormEvent } from 'react'; + +import { cn } from '@/helpers/styles'; +import { useSoundStore, usePresetStore } from '@/store'; + +import styles from './new.module.css'; + +export function New() { + const [name, setName] = useState(''); + + const noSelected = useSoundStore(state => state.noSelected()); + const sounds = useSoundStore(state => state.sounds); + const addPreset = usePresetStore(state => state.addPreset); + + const handleSubmit = (e: FormEvent) => { + e.preventDefault(); + + if (!name || noSelected) return; + + const _sounds: Record = {}; + + Object.keys(sounds) + .filter(id => sounds[id].isSelected) + .forEach(id => { + _sounds[id] = sounds[id].volume; + }); + + addPreset(name, _sounds); + + setName(''); + }; + + return ( +
+

New Preset

+ +
+ setName(e.target.value)} + /> + +
+ + {noSelected && ( +

+ To make a preset, first select some sounds. +

+ )} +
+ ); +} diff --git a/src/components/modals/presets/presets.module.css b/src/components/modals/presets/presets.module.css new file mode 100644 index 0000000..9096e74 --- /dev/null +++ b/src/components/modals/presets/presets.module.css @@ -0,0 +1,12 @@ +.title { + font-family: var(--font-heading); + font-size: var(--font-md); + font-weight: 600; +} + +.divider { + width: 100%; + height: 1px; + margin: 16px 0; + background-color: var(--color-neutral-200); +} diff --git a/src/components/modals/presets/presets.tsx b/src/components/modals/presets/presets.tsx new file mode 100644 index 0000000..d72da7c --- /dev/null +++ b/src/components/modals/presets/presets.tsx @@ -0,0 +1,21 @@ +import { Modal } from '@/components/modal'; +import { New } from './new'; +import { List } from './list'; + +import styles from './presets.module.css'; + +interface PresetsModalProps { + onClose: () => void; + show: boolean; +} + +export function PresetsModal({ onClose, show }: PresetsModalProps) { + return ( + +

Presets

+ +
+ + + ); +} diff --git a/src/store/index.ts b/src/store/index.ts index 180ffae..3825bec 100644 --- a/src/store/index.ts +++ b/src/store/index.ts @@ -2,3 +2,4 @@ export { useSoundStore } from './sound'; export { useLoadingStore } from './loading'; export { useNoteStore } from './note'; export { usePomodoroStore } from './pomodoro'; +export { usePresetStore } from './preset'; diff --git a/src/store/preset/index.ts b/src/store/preset/index.ts new file mode 100644 index 0000000..bce2281 --- /dev/null +++ b/src/store/preset/index.ts @@ -0,0 +1,33 @@ +import { create } from 'zustand'; + +interface PresetStore { + addPreset: (label: string, sounds: Record) => void; + changeName: (index: number, newName: string) => void; + deletePreset: (index: number) => void; + presets: Array<{ + label: string; + sounds: Record; + }>; +} + +export const usePresetStore = create()((set, get) => ({ + addPreset(label: string, sounds: Record) { + set({ presets: [{ label, sounds }, ...get().presets] }); + }, + + changeName(index: number, newName: string) { + const presets = get().presets.map((preset, i) => { + if (i === index) return { ...preset, label: newName }; + + return preset; + }); + + set({ presets }); + }, + + deletePreset(index: number) { + set({ presets: get().presets.filter((_, i) => index !== i) }); + }, + + presets: [], +})); diff --git a/src/store/sound/sound.actions.ts b/src/store/sound/sound.actions.ts index a167301..9c88895 100644 --- a/src/store/sound/sound.actions.ts +++ b/src/store/sound/sound.actions.ts @@ -31,11 +31,13 @@ export const createActions: StateCreator< const sounds = get().sounds; Object.keys(newSounds).forEach(sound => { - sounds[sound].isSelected = true; - sounds[sound].volume = newSounds[sound]; + if (sounds[sound]) { + sounds[sound].isSelected = true; + sounds[sound].volume = newSounds[sound]; + } }); - set({ sounds: { ...sounds } }); + set({ history: null, sounds: { ...sounds } }); }, pause() {