feat: add custom presets

This commit is contained in:
MAZE 2024-02-28 19:59:39 +03:30
parent 98d2f76438
commit 2484e01273
15 changed files with 327 additions and 3 deletions

View File

@ -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';

View File

@ -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 <Item icon={<RiPlayListFill />} label="Your Presets" onClick={open} />;
}

View File

@ -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}
>
<PresetsItem open={() => setShowPresets(true)} />
<ShareItem open={() => setShowShareLink(true)} />
<ShuffleItem />
<Divider />
@ -104,6 +108,8 @@ export function Menu() {
onClose={() => setShowShareLink(false)}
/>
<PresetsModal show={showPresets} onClose={() => setShowPresets(false)} />
<Notepad show={showNotepad} onClose={() => setShowNotepad(false)} />
<Pomodoro show={showPomodoro} onClose={() => setShowPomodoro(false)} />
</>

View File

@ -0,0 +1 @@
export { PresetsModal } from './presets';

View File

@ -0,0 +1 @@
export { List } from './list';

View File

@ -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);
}
}
}
}

View File

@ -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 (
<div className={styles.list}>
<h3 className={styles.title}>
Your Presets {presets.length > 0 && `(${presets.length})`}
</h3>
{!presets.length && (
<p className={styles.empty}>You don&apos;t have any presets yet.</p>
)}
{presets.map((preset, index) => (
<div className={styles.preset} key={index}>
<input
placeholder="Untitled"
type="text"
value={preset.label}
onChange={e => changeName(index, e.target.value)}
/>
<button onClick={() => deletePreset(index)}>
<FaRegTrashAlt />
</button>
<button
className={styles.primary}
onClick={() => {
override(preset.sounds);
play();
close();
}}
>
<FaPlay />
</button>
</div>
))}
</div>
);
}

View File

@ -0,0 +1 @@
export { New } from './new';

View File

@ -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);
}
}

View File

@ -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<HTMLFormElement>) => {
e.preventDefault();
if (!name || noSelected) return;
const _sounds: Record<string, number> = {};
Object.keys(sounds)
.filter(id => sounds[id].isSelected)
.forEach(id => {
_sounds[id] = sounds[id].volume;
});
addPreset(name, _sounds);
setName('');
};
return (
<div className={styles.new}>
<h3 className={styles.title}>New Preset</h3>
<form
className={cn(styles.form, noSelected && styles.disabled)}
onSubmit={handleSubmit}
>
<input
disabled={noSelected}
placeholder="Preset's Name"
required
type="text"
value={name}
onChange={e => setName(e.target.value)}
/>
<button disabled={noSelected}>Save</button>
</form>
{noSelected && (
<p className={styles.noSelected}>
To make a preset, first select some sounds.
</p>
)}
</div>
);
}

View File

@ -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);
}

View File

@ -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 (
<Modal show={show} onClose={onClose}>
<h2 className={styles.title}>Presets</h2>
<New />
<div className={styles.divider} />
<List close={onClose} />
</Modal>
);
}

View File

@ -2,3 +2,4 @@ export { useSoundStore } from './sound';
export { useLoadingStore } from './loading';
export { useNoteStore } from './note';
export { usePomodoroStore } from './pomodoro';
export { usePresetStore } from './preset';

33
src/store/preset/index.ts Normal file
View File

@ -0,0 +1,33 @@
import { create } from 'zustand';
interface PresetStore {
addPreset: (label: string, sounds: Record<string, number>) => void;
changeName: (index: number, newName: string) => void;
deletePreset: (index: number) => void;
presets: Array<{
label: string;
sounds: Record<string, number>;
}>;
}
export const usePresetStore = create<PresetStore>()((set, get) => ({
addPreset(label: string, sounds: Record<string, number>) {
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: [],
}));

View File

@ -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() {