-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add tagging and note connecting functionality (#5)
* add caching adjustments and disable link prefetch in navbar * refactor note actions into a standalone component * add ui for linked notes and tags, adjust schema so that users own their tags * add badges for tags * add note relation creation functionality * fix db query filter and remove indexing from results * add better note connection filters * add tag manager functionality
- Loading branch information
1 parent
be319b7
commit 6e3e937
Showing
18 changed files
with
522 additions
and
43 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,171 @@ | ||
"use client"; | ||
|
||
import { Cross1Icon, PlusCircledIcon, TrashIcon } from "@radix-ui/react-icons"; | ||
import Link from "next/link"; | ||
import { useRouter } from "next/navigation"; | ||
import { useState } from "react"; | ||
|
||
import { Badge } from "@/components/ui/badge"; | ||
import { Button } from "@/components/ui/button"; | ||
import { deleteNote } from "@/helpers/notes/deleteNote"; | ||
import { disconnectNotes } from "@/helpers/notes/disconnectNotes"; | ||
import { prepareNoteRelations } from "@/helpers/notes/prepareNoteRelations"; | ||
import { removeTag } from "@/helpers/notes/removeTag"; | ||
import { INote } from "@/types/platform"; | ||
|
||
import NoteConnector from "./note-connector"; | ||
import TagManager from "./tag-manager"; | ||
|
||
type Props = { | ||
note: INote; | ||
}; | ||
|
||
export default function NoteActions({ note }: Props) { | ||
const [showNoteConnector, setShowNoteConnector] = useState(false); | ||
const [showTagManager, setShowTagManager] = useState(false); | ||
|
||
const router = useRouter(); | ||
|
||
async function handleDeleteNote() { | ||
const deletedNote = await deleteNote(note.id); | ||
|
||
if (!deletedNote) { | ||
alert("Failed to delete note"); // TODO: better error handling with a toast | ||
return; | ||
} | ||
|
||
router.push("/notes"); | ||
} | ||
|
||
async function handleDisconnectNote(toDisconnectId: string) { | ||
const disconnectedNote = await disconnectNotes(note.id, toDisconnectId); | ||
|
||
if (!disconnectedNote) { | ||
alert("Failed to disconnect note"); // TODO: better error handling with a toast | ||
return; | ||
} | ||
|
||
router.refresh(); | ||
} | ||
|
||
async function handleDisconnectTag(tagId: string) { | ||
const disconnectedNote = await removeTag(note.id, tagId); | ||
|
||
if (!disconnectedNote) { | ||
alert("Failed to disconnect tag"); // TODO: better error handling with a toast | ||
return; | ||
} | ||
|
||
router.refresh(); | ||
} | ||
|
||
const noteRelations = prepareNoteRelations(note); | ||
|
||
return ( | ||
<> | ||
<div className="flex w-80 flex-col bg-slate-100/50 p-6 text-sm text-slate-400"> | ||
<div className="flex flex-col items-stretch gap-4"> | ||
<span className="block">Connected notes</span> | ||
{noteRelations.length === 0 ? ( | ||
<span className="text-xs text-slate-400"> | ||
No connected notes yet | ||
</span> | ||
) : ( | ||
<ul className="flex flex-col gap-2"> | ||
{noteRelations.map((relatedNote) => ( | ||
<li | ||
key={relatedNote.id} | ||
className="flex w-full items-center justify-between" | ||
> | ||
<Link href={`/notes/${relatedNote.id}`} prefetch={false}> | ||
<Button | ||
className="w-full justify-start p-0" | ||
variant={"link"} | ||
size={"sm"} | ||
> | ||
{relatedNote.title.length > 32 | ||
? `${relatedNote.title.slice(0, 32)}...` | ||
: relatedNote.title} | ||
</Button> | ||
</Link> | ||
<Button | ||
size={"sm"} | ||
variant={"link"} | ||
onClick={() => handleDisconnectNote(relatedNote.id)} | ||
> | ||
<TrashIcon /> | ||
</Button> | ||
</li> | ||
))} | ||
</ul> | ||
)} | ||
<Button | ||
className="w-full justify-start" | ||
variant={"outline"} | ||
size={"sm"} | ||
onClick={() => setShowNoteConnector(true)} | ||
> | ||
<PlusCircledIcon className="mr-3" /> | ||
Connect notes | ||
</Button> | ||
</div> | ||
<hr className="my-6 border-t border-slate-200" /> | ||
<div className="flex flex-col items-stretch gap-4"> | ||
<span className="block">Tags</span> | ||
{note.tags.length === 0 ? ( | ||
<span className="text-xs text-slate-400">No tags yet</span> | ||
) : ( | ||
<ul className="flex flex-wrap gap-2"> | ||
{note.tags.map((tag) => ( | ||
<li key={tag.id}> | ||
<Badge variant="outline"> | ||
{tag.name} | ||
<Button | ||
size={"sm"} | ||
variant={"link"} | ||
className="ml-2 h-min w-min p-0 hover:bg-slate-200" | ||
onClick={() => handleDisconnectTag(tag.id)} | ||
> | ||
<Cross1Icon className="h-[10px] w-[10px]" /> | ||
</Button> | ||
</Badge> | ||
</li> | ||
))} | ||
</ul> | ||
)} | ||
<Button | ||
className="w-full justify-start" | ||
variant={"outline"} | ||
size={"sm"} | ||
onClick={() => setShowTagManager(true)} | ||
> | ||
<PlusCircledIcon className="mr-3" /> | ||
Manage tags | ||
</Button> | ||
</div> | ||
<hr className="my-6 border-t border-slate-200" /> | ||
<div className="flex flex-col items-stretch gap-4"> | ||
<span className="block">Note actions</span> | ||
<Button | ||
className="w-full justify-start" | ||
variant={"destructive"} | ||
onClick={handleDeleteNote} | ||
> | ||
<TrashIcon className="mr-3" /> | ||
Move to trash | ||
</Button> | ||
</div> | ||
</div> | ||
<NoteConnector | ||
note={note} | ||
open={showNoteConnector} | ||
setOpen={setShowNoteConnector} | ||
/> | ||
<TagManager | ||
note={note} | ||
open={showTagManager} | ||
setOpen={setShowTagManager} | ||
/> | ||
</> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,92 @@ | ||
"use client"; | ||
|
||
import debounce from "lodash.debounce"; | ||
import { useRouter } from "next/navigation"; | ||
import { useState } from "react"; | ||
|
||
import { | ||
CommandDialog, | ||
CommandEmpty, | ||
CommandGroup, | ||
CommandInput, | ||
CommandItem, | ||
CommandList, | ||
} from "@/components/ui/command"; | ||
import { connectNotes } from "@/helpers/notes/connectNotes"; | ||
import { prepareNoteRelations } from "@/helpers/notes/prepareNoteRelations"; | ||
import { searchNotes } from "@/helpers/notes/searchNotes"; | ||
import { INote } from "@/types/platform"; | ||
|
||
type Props = { | ||
note: INote; | ||
open: boolean; | ||
setOpen: (open: boolean) => void; | ||
}; | ||
|
||
type NotesSearchResult = Awaited<ReturnType<typeof searchNotes>>; | ||
|
||
export default function NoteConnector({ note, open, setOpen }: Props) { | ||
const [results, setResults] = useState<NotesSearchResult>([]); | ||
|
||
const router = useRouter(); | ||
|
||
const { id: noteId } = note; | ||
|
||
const existingNoteRelationIds = prepareNoteRelations(note).map( | ||
(note) => note.id | ||
); | ||
|
||
async function handleSearchChange( | ||
event: React.ChangeEvent<HTMLInputElement> | ||
) { | ||
const search = event.target.value; | ||
|
||
const results = await searchNotes(search, { | ||
doNotIncludeNoteIds: [...existingNoteRelationIds, noteId], | ||
}); | ||
|
||
if (!results) return; | ||
|
||
setResults(results); | ||
} | ||
|
||
const debouncedHandleSearchChange = debounce(handleSearchChange, 500); | ||
|
||
async function handleConnectNotes(relatedNoteId: string) { | ||
await connectNotes(noteId, relatedNoteId); | ||
setOpen(false); | ||
router.refresh(); | ||
} | ||
|
||
function handleClose() { | ||
setResults([]); | ||
setOpen(false); | ||
} | ||
|
||
return ( | ||
<CommandDialog open={open} onOpenChange={handleClose}> | ||
<CommandInput | ||
onInput={debouncedHandleSearchChange} | ||
placeholder="Type a note's name to search..." | ||
/> | ||
<CommandList> | ||
<CommandEmpty>No results found.</CommandEmpty> | ||
<CommandList> | ||
{!!results.length && ( | ||
<CommandGroup heading="Suggestions"> | ||
{results.map((result) => ( | ||
<CommandItem | ||
key={result.id} | ||
onSelect={() => handleConnectNotes(result.id)} | ||
className="cursor-pointer" | ||
> | ||
{result.title} | ||
</CommandItem> | ||
))} | ||
</CommandGroup> | ||
)} | ||
</CommandList> | ||
</CommandList> | ||
</CommandDialog> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.