feat: add simple notepad

This commit is contained in:
MAZE 2024-02-24 16:06:53 +03:30
parent 38c11f124e
commit e923559709
15 changed files with 157 additions and 5 deletions

View file

@ -4,7 +4,8 @@
align-items: center; align-items: center;
justify-content: flex-start; justify-content: flex-start;
width: 100%; width: 100%;
padding: 16px 12px; height: 50px;
padding: 0 12px;
font-size: var(--font-sm); font-size: var(--font-sm);
font-weight: 500; font-weight: 500;
line-height: 1; line-height: 1;

View file

@ -1,3 +1,4 @@
export { Shuffle as ShuffleItem } from './shuffle'; export { Shuffle as ShuffleItem } from './shuffle';
export { Share as ShareItem } from './share'; export { Share as ShareItem } from './share';
export { Donate as DonateItem } from './donate'; export { Donate as DonateItem } from './donate';
export { Notepad as NotepadItem } from './notepad';

View file

@ -0,0 +1,11 @@
import { MdNotes } from 'react-icons/md/index';
import { Item } from '../item';
interface NotepadProps {
open: () => void;
}
export function Notepad({ open }: NotepadProps) {
return <Item icon={<MdNotes />} label="Notepad" onClick={open} />;
}

View file

@ -14,8 +14,9 @@ import {
FloatingFocusManager, FloatingFocusManager,
} from '@floating-ui/react'; } from '@floating-ui/react';
import { ShuffleItem, ShareItem, DonateItem } from './items'; import { ShuffleItem, ShareItem, DonateItem, NotepadItem } from './items';
import { ShareLinkModal } from '@/components/modals/share-link'; import { ShareLinkModal } from '@/components/modals/share-link';
import { Notepad } from '@/components/toolbox';
import { slideY, fade, mix } from '@/lib/motion'; import { slideY, fade, mix } from '@/lib/motion';
@ -25,6 +26,7 @@ export function Menu() {
const [isOpen, setIsOpen] = useState(false); const [isOpen, setIsOpen] = useState(false);
const [showShareLink, setShowShareLink] = useState(false); const [showShareLink, setShowShareLink] = useState(false);
const [showNotepad, setShowNotepad] = useState(false);
const variants = mix(slideY(-20), fade()); const variants = mix(slideY(-20), fade());
@ -76,6 +78,7 @@ export function Menu() {
> >
<ShareItem open={() => setShowShareLink(true)} /> <ShareItem open={() => setShowShareLink(true)} />
<ShuffleItem /> <ShuffleItem />
<NotepadItem open={() => setShowNotepad(true)} />
<DonateItem /> <DonateItem />
</motion.div> </motion.div>
</div> </div>
@ -88,6 +91,8 @@ export function Menu() {
show={showShareLink} show={showShareLink}
onClose={() => setShowShareLink(false)} onClose={() => setShowShareLink(false)}
/> />
<Notepad show={showNotepad} onClose={() => setShowNotepad(false)} />
</> </>
); );
} }

View file

@ -29,6 +29,13 @@
background-color: var(--color-neutral-100); background-color: var(--color-neutral-100);
border-radius: 8px; border-radius: 8px;
&.wide {
width: 95%;
max-width: 600px;
padding: 12px;
padding-top: 40px;
}
& .close { & .close {
position: absolute; position: absolute;
top: 10px; top: 10px;

View file

@ -2,6 +2,7 @@ import { AnimatePresence, motion } from 'framer-motion';
import { IoClose } from 'react-icons/io5/index'; import { IoClose } from 'react-icons/io5/index';
import { fade, mix, slideY } from '@/lib/motion'; import { fade, mix, slideY } from '@/lib/motion';
import { cn } from '@/helpers/styles';
import styles from './modal.module.css'; import styles from './modal.module.css';
@ -9,9 +10,10 @@ interface ModalProps {
children: React.ReactNode; children: React.ReactNode;
onClose: () => void; onClose: () => void;
show: boolean; show: boolean;
wide?: boolean;
} }
export function Modal({ children, onClose, show }: ModalProps) { export function Modal({ children, onClose, show, wide }: ModalProps) {
const variants = { const variants = {
modal: mix(fade(), slideY(20)), modal: mix(fade(), slideY(20)),
overlay: fade(), overlay: fade(),
@ -33,7 +35,7 @@ export function Modal({ children, onClose, show }: ModalProps) {
<div className={styles.modal}> <div className={styles.modal}>
<motion.div <motion.div
animate="show" animate="show"
className={styles.content} className={cn(styles.content, wide && styles.wide)}
exit="hidden" exit="hidden"
initial="hidden" initial="hidden"
variants={variants.modal} variants={variants.modal}

View file

@ -1,6 +1,6 @@
import { useEffect } from 'react'; import { useEffect } from 'react';
import { useSoundStore } from '@/store'; import { useSoundStore, useNoteStore } from '@/store';
interface StoreConsumerProps { interface StoreConsumerProps {
children: React.ReactNode; children: React.ReactNode;
@ -9,6 +9,7 @@ interface StoreConsumerProps {
export function StoreConsumer({ children }: StoreConsumerProps) { export function StoreConsumer({ children }: StoreConsumerProps) {
useEffect(() => { useEffect(() => {
useSoundStore.persist.rehydrate(); useSoundStore.persist.rehydrate();
useNoteStore.persist.rehydrate();
}, []); }, []);
return <>{children}</>; return <>{children}</>;

View file

@ -0,0 +1 @@
export { Notepad } from './notepad';

View file

@ -0,0 +1 @@
export { Notepad } from './notepad';

View file

@ -0,0 +1,26 @@
.header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 8px;
& .label {
font-size: var(--font-sm);
font-weight: 500;
color: var(--color-foreground-subtle);
}
}
.textarea {
width: 100%;
height: 350px;
padding: 12px;
line-height: 1.6;
color: var(--color-foreground-subtle);
resize: none;
background-color: var(--color-neutral-50);
border: 1px solid var(--color-neutral-200);
border-radius: 4px;
outline: none;
scroll-padding-bottom: 12px;
}

View file

@ -0,0 +1,29 @@
import { Modal } from '@/components/modal';
import styles from './notepad.module.css';
import { useNoteStore } from '@/store';
interface NotepadProps {
onClose: () => void;
show: boolean;
}
export function Notepad({ onClose, show }: NotepadProps) {
const note = useNoteStore(state => state.note);
const write = useNoteStore(state => state.write);
return (
<Modal show={show} wide onClose={onClose}>
<header className={styles.header}>
<h2 className={styles.label}>Your Note</h2>
</header>
<textarea
className={styles.textarea}
dir="auto"
value={note}
onChange={e => write(e.target.value)}
/>
</Modal>
);
}

View file

@ -1,2 +1,3 @@
export { useSoundStore } from './sound'; export { useSoundStore } from './sound';
export { useLoadingStore } from './loading'; export { useLoadingStore } from './loading';
export { useNoteStore } from './note';

28
src/store/note/index.ts Normal file
View file

@ -0,0 +1,28 @@
import { create } from 'zustand';
import { createJSONStorage, persist } from 'zustand/middleware';
import merge from 'deepmerge';
import { type NoteState, createState } from './note.state';
import { type NoteActions, createActions } from './note.actions';
export const useNoteStore = create<NoteState & NoteActions>()(
persist(
(...a) => ({
...createState(...a),
...createActions(...a),
}),
{
merge: (persisted, current) =>
merge(
current,
// @ts-ignore
persisted,
),
name: 'moodist-note',
partialize: state => ({ note: state.note }),
skipHydration: true,
storage: createJSONStorage(() => localStorage),
version: 0,
},
),
);

View file

@ -0,0 +1,20 @@
import type { StateCreator } from 'zustand';
import type { NoteState } from './note.state';
export interface NoteActions {
write: (note: string) => void;
}
export const createActions: StateCreator<
NoteActions & NoteState,
[],
[],
NoteActions
> = set => {
return {
write(note) {
set({ note });
},
};
};

View file

@ -0,0 +1,18 @@
import type { StateCreator } from 'zustand';
import type { NoteActions } from './note.actions';
export interface NoteState {
history: string | null;
note: string;
}
export const createState: StateCreator<
NoteState & NoteActions,
[],
[],
NoteState
> = () => ({
history: null,
note: '',
});