Skip to content

Commit a6f5a6d

Browse files
authored
Redesign adding a new note (#351)
1 parent 53f154a commit a6f5a6d

19 files changed

+427
-109
lines changed
Lines changed: 96 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,24 @@
1-
import { memo, useCallback, useContext, useEffect, useRef, useState } from "react";
1+
import { memo, useCallback, useContext, useEffect, useRef } from "react";
22
import "./NoteManager.css";
33
import type { ISynctexBlockId } from "@fluffylabs/links-metadata";
4-
import { Button, Textarea } from "@fluffylabs/shared-ui";
4+
import { cn } from "@fluffylabs/shared-ui";
55
import { twMerge } from "tailwind-merge";
6-
import { validateMath } from "../../utils/validateMath";
6+
import { useLatestCallback } from "../../hooks/useLatestCallback";
77
import { type ILocationContext, LocationContext } from "../LocationProvider/LocationProvider";
88
import { type INotesContext, NotesContext } from "../NotesProvider/NotesProvider";
9-
import { LABEL_LOCAL } from "../NotesProvider/consts/labels";
109
import type { IDecoratedNote } from "../NotesProvider/types/DecoratedNote";
1110
import type { IStorageNote } from "../NotesProvider/types/StorageNote";
12-
import { Selection } from "../Selection/Selection";
11+
import { areSelectionsEqual } from "../NotesProvider/utils/areSelectionsEqual";
1312
import { type ISelectionContext, SelectionContext } from "../SelectionProvider/SelectionProvider";
13+
import { InactiveNoteSkeleton } from "./components/InactiveNoteSkeleton";
14+
import { NewNote } from "./components/NewNote";
1415
import { NotesList } from "./components/NotesList";
1516

1617
const DEFAULT_AUTHOR = "";
1718

1819
export function NoteManager({ className }: { className?: string }) {
1920
return (
2021
<div className={twMerge("notes-wrapper gap-4", className)}>
21-
<Selection />
2222
<Notes />
2323
</div>
2424
);
@@ -27,62 +27,66 @@ export function NoteManager({ className }: { className?: string }) {
2727
const MemoizedNotesList = memo(NotesList);
2828

2929
function Notes() {
30-
const [noteContent, setNoteContent] = useState("");
31-
const [noteContentError, setNoteContentError] = useState("");
3230
const { locationParams, setLocationParams } = useContext(LocationContext) as ILocationContext;
3331
const { notesReady, activeNotes, notes, handleAddNote, handleDeleteNote, handleUpdateNote } = useContext(
3432
NotesContext,
3533
) as INotesContext;
3634
const { selectedBlocks, pageNumber, handleClearSelection } = useContext(SelectionContext) as ISelectionContext;
35+
const keepShowingNewNote = useRef<{ selectionEnd: ISynctexBlockId; selectionStart: ISynctexBlockId }>(undefined);
3736

38-
const handleAddNoteRef = useRef(handleAddNote);
39-
handleAddNoteRef.current = handleAddNote;
40-
const handleDeleteNoteRef = useRef(handleDeleteNote);
41-
handleDeleteNoteRef.current = handleDeleteNote;
42-
const handleUpdateNoteRef = useRef(handleUpdateNote);
43-
handleUpdateNoteRef.current = handleUpdateNote;
44-
45-
const memoizedHandleDeleteNote = useCallback((note: IDecoratedNote) => {
46-
handleDeleteNoteRef.current(note);
47-
}, []);
48-
49-
const memoizedHandleUpdateNote = useCallback((note: IDecoratedNote, newNote: IStorageNote) => {
50-
handleUpdateNoteRef.current(note, newNote);
51-
}, []);
52-
53-
const handleAddNoteClick = useCallback(() => {
54-
if (
55-
selectedBlocks.length === 0 ||
56-
pageNumber === null ||
57-
!locationParams.selectionStart ||
58-
!locationParams.selectionEnd
59-
) {
60-
throw new Error("Attempted saving a note without selection.");
61-
}
62-
63-
setNoteContentError("");
37+
const latestHandleAddNote = useLatestCallback(handleAddNote);
38+
const latestDeleteNote = useLatestCallback(handleDeleteNote);
39+
const latestUpdateNote = useLatestCallback(handleUpdateNote);
6440

65-
const mathValidationError = validateMath(noteContent);
41+
const memoizedHandleDeleteNote = useCallback(
42+
(note: IDecoratedNote) => {
43+
latestDeleteNote.current(note);
44+
handleClearSelection();
45+
},
46+
[handleClearSelection, latestDeleteNote],
47+
);
6648

67-
if (mathValidationError) {
68-
setNoteContentError(mathValidationError);
69-
return;
70-
}
49+
const memoizedHandleUpdateNote = useCallback(
50+
(note: IDecoratedNote, newNote: IStorageNote) => {
51+
latestUpdateNote.current(note, newNote);
52+
},
53+
[latestUpdateNote],
54+
);
7155

72-
const newNote: IStorageNote = {
73-
noteVersion: 3,
74-
content: noteContent,
75-
date: Date.now(),
76-
author: DEFAULT_AUTHOR,
77-
selectionStart: locationParams.selectionStart,
78-
selectionEnd: locationParams.selectionEnd,
79-
version: locationParams.version,
80-
labels: [LABEL_LOCAL],
81-
};
82-
83-
handleAddNoteRef.current(newNote);
56+
const handleNewNoteCancel = useCallback(() => {
8457
handleClearSelection();
85-
}, [noteContent, pageNumber, selectedBlocks, handleClearSelection, locationParams]);
58+
}, [handleClearSelection]);
59+
60+
const handleAddNoteClick = useCallback(
61+
({ noteContent, labels }: { noteContent: string; labels: string[] }) => {
62+
if (
63+
selectedBlocks.length === 0 ||
64+
pageNumber === null ||
65+
!locationParams.selectionStart ||
66+
!locationParams.selectionEnd
67+
) {
68+
throw new Error("Attempted saving a note without selection.");
69+
}
70+
71+
const newNote: IStorageNote = {
72+
noteVersion: 3,
73+
content: noteContent,
74+
date: Date.now(),
75+
author: DEFAULT_AUTHOR,
76+
selectionStart: locationParams.selectionStart,
77+
selectionEnd: locationParams.selectionEnd,
78+
version: locationParams.version,
79+
labels,
80+
};
81+
82+
latestHandleAddNote.current(newNote);
83+
keepShowingNewNote.current = {
84+
selectionStart: locationParams.selectionStart,
85+
selectionEnd: locationParams.selectionEnd,
86+
};
87+
},
88+
[pageNumber, selectedBlocks, locationParams, latestHandleAddNote],
89+
);
8690

8791
const locationRef = useRef({ locationParams, setLocationParams });
8892
locationRef.current = { locationParams, setLocationParams };
@@ -113,38 +117,51 @@ function Notes() {
113117
[],
114118
);
115119

120+
const isActiveNotes = notes.some((note) => activeNotes.includes(note));
121+
116122
useEffect(() => {
117-
if (selectedBlocks.length === 0) {
118-
setNoteContent("");
119-
setNoteContentError("");
123+
if (notesReady) {
124+
keepShowingNewNote.current = undefined;
120125
}
121-
}, [selectedBlocks]);
126+
}, [notesReady]);
122127

123128
return (
124-
<div className="note-manager flex flex-col gap-2.5" style={{ opacity: notesReady ? 1.0 : 0.3 }}>
125-
<div className="flex flex-col p-2 gap-2">
126-
<Textarea
127-
disabled={selectedBlocks.length === 0}
128-
className={noteContentError ? "error" : ""}
129-
autoFocus
130-
value={noteContent}
131-
onChange={(ev) => setNoteContent(ev.currentTarget.value)}
132-
placeholder="Add a note to the selected fragment. Math typesetting is supported! Use standard delimiters such as $...$, \[...\] or \begin{equation}...\end{equation}."
129+
<div className={cn("note-manager flex flex-col gap-2.5", !notesReady && "opacity-30 pointer-events-none")}>
130+
{locationParams.selectionEnd &&
131+
locationParams.selectionStart &&
132+
pageNumber !== null &&
133+
selectedBlocks.length > 0 &&
134+
!isActiveNotes &&
135+
(notesReady || areSelectionsEqual(locationParams, keepShowingNewNote.current)) && (
136+
<NewNote
137+
selectionStart={locationParams.selectionStart}
138+
selectionEnd={locationParams.selectionEnd}
139+
version={locationParams.version}
140+
onCancel={handleNewNoteCancel}
141+
onSave={handleAddNoteClick}
142+
/>
143+
)}
144+
145+
{!notesReady && notes.length === 0 && (
146+
<>
147+
<InactiveNoteSkeleton />
148+
<InactiveNoteSkeleton />
149+
<InactiveNoteSkeleton />
150+
<InactiveNoteSkeleton />
151+
</>
152+
)}
153+
154+
{notesReady && notes.length === 0 && <div className="no-notes text-sidebar-foreground">No notes available</div>}
155+
156+
{notes.length > 0 && (
157+
<MemoizedNotesList
158+
activeNotes={activeNotes}
159+
notes={notes}
160+
onEditNote={memoizedHandleUpdateNote}
161+
onDeleteNote={memoizedHandleDeleteNote}
162+
onSelectNote={memoizedHandleSelectNote}
133163
/>
134-
135-
{noteContentError ? <div className="validation-message">{noteContentError}</div> : null}
136-
<Button disabled={noteContent.length < 1} onClick={handleAddNoteClick} variant="secondary">
137-
Add
138-
</Button>
139-
</div>
140-
141-
<MemoizedNotesList
142-
activeNotes={activeNotes}
143-
notes={notes}
144-
onEditNote={memoizedHandleUpdateNote}
145-
onDeleteNote={memoizedHandleDeleteNote}
146-
onSelectNote={memoizedHandleSelectNote}
147-
/>
164+
)}
148165
</div>
149166
);
150167
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { cn } from "@fluffylabs/shared-ui";
2+
import { NoteContainer } from "./SimpleComponents/NoteContainer";
3+
4+
const skeletonLine = "rounded-md bg-gray-300/100 dark:bg-white/10";
5+
6+
export const InactiveNoteSkeleton = ({ className }: { className?: string }) => (
7+
<NoteContainer
8+
active={false}
9+
aria-hidden="true"
10+
className={cn(
11+
"note relative rounded-xl p-4 bg-[var(--inactive-note-bg)] border border-[var(--border)]/60 select-none animate-pulse",
12+
className,
13+
)}
14+
>
15+
<div className="flex flex-col gap-3">
16+
<div className="flex items-center gap-2">
17+
<div className={cn("h-4 w-9/12 rounded-md bg-brand-primary/50")} />
18+
</div>
19+
<div className="space-y-1">
20+
<div className={cn("h-4 w-full", skeletonLine)} />
21+
<div className={cn("h-4 w-11/12", skeletonLine)} />
22+
<div className={cn("h-4 w-8/12", skeletonLine)} />
23+
</div>
24+
</div>
25+
</NoteContainer>
26+
);

0 commit comments

Comments
 (0)