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 { Source as SourceItem } from './source';
|
||||
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,
|
||||
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)} />
|
||||
</>
|
||||
|
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 { useNoteStore } from './note';
|
||||
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;
|
||||
|
||||
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() {
|
||||
|
Loading…
x
Reference in New Issue
Block a user