mirror of
https://github.com/remvze/moodist.git
synced 2025-09-29 15:30:49 -04:00
feat: add custom presets
This commit is contained in:
parent
98d2f76438
commit
2484e01273
@ -4,3 +4,4 @@ export { Donate as DonateItem } from './donate';
|
|||||||
export { Notepad as NotepadItem } from './notepad';
|
export { Notepad as NotepadItem } from './notepad';
|
||||||
export { Source as SourceItem } from './source';
|
export { Source as SourceItem } from './source';
|
||||||
export { Pomodoro as PomodoroItem } from './pomodoro';
|
export { Pomodoro as PomodoroItem } from './pomodoro';
|
||||||
|
export { Presets as PresetsItem } from './presets';
|
||||||
|
11
src/components/menu/items/presets.tsx
Normal file
11
src/components/menu/items/presets.tsx
Normal 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} />;
|
||||||
|
}
|
@ -21,9 +21,11 @@ import {
|
|||||||
NotepadItem,
|
NotepadItem,
|
||||||
SourceItem,
|
SourceItem,
|
||||||
PomodoroItem,
|
PomodoroItem,
|
||||||
|
PresetsItem,
|
||||||
} from './items';
|
} from './items';
|
||||||
import { Divider } from './divider';
|
import { Divider } from './divider';
|
||||||
import { ShareLinkModal } from '@/components/modals/share-link';
|
import { ShareLinkModal } from '@/components/modals/share-link';
|
||||||
|
import { PresetsModal } from '@/components/modals/presets';
|
||||||
import { Notepad, Pomodoro } from '@/components/toolbox';
|
import { Notepad, Pomodoro } from '@/components/toolbox';
|
||||||
|
|
||||||
import styles from './menu.module.css';
|
import styles from './menu.module.css';
|
||||||
@ -31,6 +33,7 @@ import styles from './menu.module.css';
|
|||||||
export function Menu() {
|
export function Menu() {
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
|
||||||
|
const [showPresets, setShowPresets] = useState(false);
|
||||||
const [showShareLink, setShowShareLink] = useState(false);
|
const [showShareLink, setShowShareLink] = useState(false);
|
||||||
const [showNotepad, setShowNotepad] = useState(false);
|
const [showNotepad, setShowNotepad] = useState(false);
|
||||||
const [showPomodoro, setShowPomodoro] = useState(false);
|
const [showPomodoro, setShowPomodoro] = useState(false);
|
||||||
@ -86,6 +89,7 @@ export function Menu() {
|
|||||||
{...getFloatingProps()}
|
{...getFloatingProps()}
|
||||||
className={styles.menu}
|
className={styles.menu}
|
||||||
>
|
>
|
||||||
|
<PresetsItem open={() => setShowPresets(true)} />
|
||||||
<ShareItem open={() => setShowShareLink(true)} />
|
<ShareItem open={() => setShowShareLink(true)} />
|
||||||
<ShuffleItem />
|
<ShuffleItem />
|
||||||
<Divider />
|
<Divider />
|
||||||
@ -104,6 +108,8 @@ export function Menu() {
|
|||||||
onClose={() => setShowShareLink(false)}
|
onClose={() => setShowShareLink(false)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<PresetsModal show={showPresets} onClose={() => setShowPresets(false)} />
|
||||||
|
|
||||||
<Notepad show={showNotepad} onClose={() => setShowNotepad(false)} />
|
<Notepad show={showNotepad} onClose={() => setShowNotepad(false)} />
|
||||||
<Pomodoro show={showPomodoro} onClose={() => setShowPomodoro(false)} />
|
<Pomodoro show={showPomodoro} onClose={() => setShowPomodoro(false)} />
|
||||||
</>
|
</>
|
||||||
|
1
src/components/modals/presets/index.ts
Normal file
1
src/components/modals/presets/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export { PresetsModal } from './presets';
|
1
src/components/modals/presets/list/index.ts
Normal file
1
src/components/modals/presets/list/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export { List } from './list';
|
61
src/components/modals/presets/list/list.module.css
Normal file
61
src/components/modals/presets/list/list.module.css
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
53
src/components/modals/presets/list/list.tsx
Normal file
53
src/components/modals/presets/list/list.tsx
Normal 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'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>
|
||||||
|
);
|
||||||
|
}
|
1
src/components/modals/presets/new/index.ts
Normal file
1
src/components/modals/presets/new/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export { New } from './new';
|
61
src/components/modals/presets/new/new.module.css
Normal file
61
src/components/modals/presets/new/new.module.css
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
59
src/components/modals/presets/new/new.tsx
Normal file
59
src/components/modals/presets/new/new.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
12
src/components/modals/presets/presets.module.css
Normal file
12
src/components/modals/presets/presets.module.css
Normal 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);
|
||||||
|
}
|
21
src/components/modals/presets/presets.tsx
Normal file
21
src/components/modals/presets/presets.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
@ -2,3 +2,4 @@ export { useSoundStore } from './sound';
|
|||||||
export { useLoadingStore } from './loading';
|
export { useLoadingStore } from './loading';
|
||||||
export { useNoteStore } from './note';
|
export { useNoteStore } from './note';
|
||||||
export { usePomodoroStore } from './pomodoro';
|
export { usePomodoroStore } from './pomodoro';
|
||||||
|
export { usePresetStore } from './preset';
|
||||||
|
33
src/store/preset/index.ts
Normal file
33
src/store/preset/index.ts
Normal 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: [],
|
||||||
|
}));
|
@ -31,11 +31,13 @@ export const createActions: StateCreator<
|
|||||||
const sounds = get().sounds;
|
const sounds = get().sounds;
|
||||||
|
|
||||||
Object.keys(newSounds).forEach(sound => {
|
Object.keys(newSounds).forEach(sound => {
|
||||||
sounds[sound].isSelected = true;
|
if (sounds[sound]) {
|
||||||
sounds[sound].volume = newSounds[sound];
|
sounds[sound].isSelected = true;
|
||||||
|
sounds[sound].volume = newSounds[sound];
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
set({ sounds: { ...sounds } });
|
set({ history: null, sounds: { ...sounds } });
|
||||||
},
|
},
|
||||||
|
|
||||||
pause() {
|
pause() {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user