Skip to content

Commit d3c54d0

Browse files
committed
feat(web): notes
1 parent 6689cab commit d3c54d0

38 files changed

+2726
-1853
lines changed

package-lock.json

Lines changed: 185 additions & 167 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/filen-web/package.json

Lines changed: 18 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
"preview": "cross-env NODE_OPTIONS=--max_old_space_size=16384 vite preview --host --port 3000"
2121
},
2222
"dependencies": {
23-
"@filen/sdk-rs": "^0.3.18",
23+
"@filen/sdk-rs": "^0.3.20",
2424
"@hookform/resolvers": "^5.2.2",
2525
"@radix-ui/react-alert-dialog": "^1.1.15",
2626
"@radix-ui/react-avatar": "^1.1.10",
@@ -43,21 +43,21 @@
4343
"@tanstack/query-persist-client-core": "^5.90.2",
4444
"@tanstack/react-query": "^5.90.2",
4545
"@tanstack/react-query-persist-client": "^5.90.2",
46-
"@tanstack/react-router": "^1.132.7",
47-
"@tanstack/react-router-devtools": "^1.132.7",
48-
"@tanstack/router-plugin": "^1.132.7",
46+
"@tanstack/react-router": "^1.132.26",
47+
"@tanstack/react-router-devtools": "^1.132.26",
48+
"@tanstack/router-plugin": "^1.132.26",
4949
"@uidotdev/usehooks": "^2.4.1",
5050
"@uiw/codemirror-extensions-langs": "^4.25.2",
5151
"@uiw/codemirror-themes-all": "^4.25.2",
5252
"@uiw/react-codemirror": "^4.25.2",
5353
"@uiw/react-md-editor": "^4.0.8",
54-
"@zip.js/zip.js": "^2.8.2",
54+
"@zip.js/zip.js": "^2.8.3",
5555
"arktype": "^2.1.22",
5656
"class-variance-authority": "^0.7.1",
5757
"clsx": "^2.1.1",
5858
"comlink": "^4.4.2",
5959
"dexie": "^4.2.0",
60-
"docx-preview": "^0.3.6",
60+
"docx-preview": "^0.3.7",
6161
"dompurify": "^3.2.7",
6262
"eventemitter3": "^5.0.1",
6363
"i18next": "^25.5.2",
@@ -87,35 +87,35 @@
8787
},
8888
"devDependencies": {
8989
"@playwright/test": "^1.55.1",
90-
"@tanstack/eslint-plugin-query": "^5.90.1",
91-
"@tanstack/eslint-plugin-router": "^1.132.0",
90+
"@tanstack/eslint-plugin-query": "^5.91.0",
91+
"@tanstack/eslint-plugin-router": "^1.132.21",
9292
"@testing-library/dom": "^10.4.1",
9393
"@testing-library/react": "^16.3.0",
9494
"@types/mime": "^4.0.0",
95-
"@types/node": "^24.5.2",
96-
"@types/react": "^19.1.14",
95+
"@types/node": "^24.6.0",
96+
"@types/react": "^19.1.16",
9797
"@types/react-dom": "^19.1.9",
98-
"@types/serviceworker": "^0.0.153",
98+
"@types/serviceworker": "^0.0.154",
9999
"@types/uuid": "^11.0.0",
100100
"@types/wicg-file-system-access": "^2023.10.6",
101-
"@typescript-eslint/eslint-plugin": "^8.44.1",
102-
"@typescript-eslint/parser": "^8.44.1",
101+
"@typescript-eslint/eslint-plugin": "^8.45.0",
102+
"@typescript-eslint/parser": "^8.45.0",
103103
"@vitejs/plugin-legacy": "^7.2.1",
104-
"@vitejs/plugin-react": "^5.0.3",
104+
"@vitejs/plugin-react": "^5.0.4",
105105
"babel-plugin-react-compiler": "^19.1.0-rc.3",
106-
"cross-env": "^10.0.0",
107-
"dotenv": "^17.2.2",
106+
"cross-env": "^10.1.0",
107+
"dotenv": "^17.2.3",
108108
"eslint": "^9.36.0",
109109
"eslint-plugin-react": "^7.37.5",
110110
"eslint-plugin-react-hooks": "^5.2.0",
111111
"i18n-ai-translate": "^4.1.2",
112112
"jsdom": "^27.0.0",
113-
"npm-check-updates": "^18.3.0",
113+
"npm-check-updates": "^19.0.0",
114114
"rimraf": "^6.0.1",
115115
"tsx": "^4.20.6",
116116
"typescript": "5.9",
117117
"vite": "^7.1.7",
118-
"vite-plugin-checker": "^0.10.3",
118+
"vite-plugin-checker": "^0.11.0",
119119
"vite-plugin-comlink": "^5.3.0",
120120
"vite-plugin-node-polyfills": "^0.24.0",
121121
"vite-plugin-pwa": "^1.0.3",

packages/filen-web/src/components/drive/list/item/index.tsx

Lines changed: 17 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import useDirectorySizeQuery from "@/queries/useDirectorySize.query"
1515
import Thumbnail from "@/components/thumbnail"
1616
import { Checkbox } from "@/components/ui/checkbox"
1717
import { useSelectDriveItemPromptStore } from "@/components/prompts/selectDriveItem"
18+
import useDragAndDrop from "@/hooks/useDragAndDrop"
1819

1920
export type DriveListItemFrom = "drive" | "select" | "search"
2021

@@ -264,19 +265,18 @@ export const DriveListItem = memo(
264265
[item, from]
265266
)
266267

267-
const onDragStart = useCallback(() => {
268-
if (from !== "drive") {
269-
return
270-
}
271-
272-
useDriveStore.getState().setSelectedItems(prev => [...prev.filter(i => i.data.uuid !== item.data.uuid), item])
273-
useDriveStore
274-
.getState()
275-
.setDraggingItems([...useDriveStore.getState().selectedItems.filter(i => i.data.uuid !== item.data.uuid), item])
276-
}, [item, from])
268+
const dragAndDrop = useDragAndDrop({
269+
start: () => {
270+
if (from !== "drive") {
271+
return
272+
}
277273

278-
const onDragOver = useCallback(
279-
(e: React.DragEvent) => {
274+
useDriveStore.getState().setSelectedItems(prev => [...prev.filter(i => i.data.uuid !== item.data.uuid), item])
275+
useDriveStore
276+
.getState()
277+
.setDraggingItems([...useDriveStore.getState().selectedItems.filter(i => i.data.uuid !== item.data.uuid), item])
278+
},
279+
over: e => {
280280
if (item.type !== "directory" || from !== "drive") {
281281
return
282282
}
@@ -291,11 +291,7 @@ export const DriveListItem = memo(
291291

292292
setDraggingOver(true)
293293
},
294-
[item, from]
295-
)
296-
297-
const onDragLeave = useCallback(
298-
(e: React.DragEvent) => {
294+
leave: e => {
299295
if (item.type !== "directory" || from !== "drive") {
300296
return
301297
}
@@ -304,11 +300,7 @@ export const DriveListItem = memo(
304300

305301
setDraggingOver(false)
306302
},
307-
[item, from]
308-
)
309-
310-
const onDrop = useCallback(
311-
(e: React.DragEvent) => {
303+
drop: e => {
312304
e.preventDefault()
313305

314306
try {
@@ -334,9 +326,8 @@ export const DriveListItem = memo(
334326
useDriveStore.getState().setSelectedItems([])
335327
useDriveStore.getState().setDraggingItems([])
336328
}
337-
},
338-
[item, from]
339-
)
329+
}
330+
})
340331

341332
return (
342333
<div
@@ -345,10 +336,7 @@ export const DriveListItem = memo(
345336
onDoubleClick={onDoubleClick}
346337
data-uuid={item.data.uuid}
347338
draggable={from === "drive"}
348-
onDragStart={onDragStart}
349-
onDragOver={onDragOver}
350-
onDragLeave={onDragLeave}
351-
onDrop={onDrop}
339+
{...dragAndDrop}
352340
>
353341
<MenuWrapper
354342
onOpenChange={onContextMenuOpenChange}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { memo } from "react"
2+
import Header from "./header"
3+
import type { Note } from "@filen/sdk-rs"
4+
5+
export const Container = memo(({ note, children }: { note: Note; children: React.ReactNode }) => {
6+
return (
7+
<div className="flex flex-1 w-full h-full flex-col overflow-hidden">
8+
<Header note={note} />
9+
<div className="flex flex-1 flex-row overflow-x-hidden overflow-y-auto h-full w-full rounded-b-lg">{children}</div>
10+
</div>
11+
)
12+
})
13+
14+
Container.displayName = "Container"
15+
16+
export default Container
Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
import { memo, useCallback, useMemo } from "react"
2+
import libNotes from "@/lib/notes"
3+
import toasts from "@/lib/toasts"
4+
import { Button } from "../../ui/button"
5+
import { NotebookIcon, PlusIcon } from "lucide-react"
6+
import useNotesQuery from "@/queries/useNotes.query"
7+
import { parseNumbersFromString } from "@/lib/utils"
8+
import { useNavigate } from "@tanstack/react-router"
9+
import type { NoteType } from "@filen/sdk-rs"
10+
import { useTranslation } from "react-i18next"
11+
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "../../ui/dropdown-menu"
12+
import Icon from "../icon"
13+
14+
export const Empty = memo(() => {
15+
const navigate = useNavigate()
16+
const { t } = useTranslation()
17+
18+
const notesQuery = useNotesQuery({
19+
enabled: false
20+
})
21+
22+
const notes = useMemo(() => {
23+
if (notesQuery.status !== "success") {
24+
return []
25+
}
26+
27+
return notesQuery.data.sort((a, b) => {
28+
if (a.pinned !== b.pinned) {
29+
return b.pinned ? 1 : -1
30+
}
31+
32+
if (a.trash !== b.trash && a.archive === false) {
33+
return a.trash ? 1 : -1
34+
}
35+
36+
if (a.archive !== b.archive) {
37+
return a.archive ? 1 : -1
38+
}
39+
40+
if (a.trash !== b.trash) {
41+
return a.trash ? 1 : -1
42+
}
43+
44+
if (b.editedTimestamp === a.editedTimestamp) {
45+
return parseNumbersFromString(b.uuid) - parseNumbersFromString(a.uuid)
46+
}
47+
48+
return Number(b.editedTimestamp) - Number(a.editedTimestamp)
49+
})
50+
}, [notesQuery.data, notesQuery.status])
51+
52+
const openLatest = useCallback(() => {
53+
if (notes.length === 0) {
54+
return
55+
}
56+
57+
const latestNote = notes.at(0)
58+
59+
if (!latestNote) {
60+
return
61+
}
62+
63+
navigate({
64+
to: "/notes/$",
65+
params: {
66+
_splat: latestNote.uuid
67+
}
68+
})
69+
}, [navigate, notes])
70+
71+
const create = useCallback(
72+
async (type?: NoteType) => {
73+
try {
74+
const note = await libNotes.create(type)
75+
76+
navigate({
77+
to: "/notes/$",
78+
params: {
79+
_splat: note.uuid
80+
}
81+
})
82+
} catch (e) {
83+
console.error(e)
84+
toasts.error(e)
85+
}
86+
},
87+
[navigate]
88+
)
89+
90+
return (
91+
<div className="flex flex-1 flex-col gap-4 items-center justify-center text-muted-foreground px-8">
92+
<NotebookIcon className="text-muted-foreground shrink size-20" />
93+
<p className="text-muted-foreground text-sm text-center max-w-[50%]">tbd</p>
94+
<div className="flex flex-col gap-1">
95+
<DropdownMenu>
96+
<DropdownMenuTrigger asChild={true}>
97+
<Button
98+
size="sm"
99+
variant="default"
100+
>
101+
<PlusIcon />
102+
tbd
103+
</Button>
104+
</DropdownMenuTrigger>
105+
<DropdownMenuContent
106+
side="right"
107+
align="start"
108+
>
109+
<DropdownMenuItem onClick={() => create("text")}>
110+
<div className="flex flex-row items-center gap-8 w-full justify-between">
111+
<p>{t("notes.menu.typeText")}</p>
112+
<Icon
113+
type="text"
114+
className="size-[14px]"
115+
/>
116+
</div>
117+
</DropdownMenuItem>
118+
<DropdownMenuItem onClick={() => create("rich")}>
119+
<div className="flex flex-row items-center gap-8 w-full justify-between">
120+
<p>{t("notes.menu.typeRichtext")}</p>
121+
<Icon
122+
type="rich"
123+
className="size-[14px]"
124+
/>
125+
</div>
126+
</DropdownMenuItem>
127+
<DropdownMenuItem onClick={() => create("checklist")}>
128+
<div className="flex flex-row items-center gap-8 w-full justify-between">
129+
<p>{t("notes.menu.typeChecklist")}</p>
130+
<Icon
131+
type="checklist"
132+
className="size-[14px]"
133+
/>
134+
</div>
135+
</DropdownMenuItem>
136+
<DropdownMenuItem onClick={() => create("md")}>
137+
<div className="flex flex-row items-center gap-8 w-full justify-between">
138+
<p>{t("notes.menu.typeMarkdown")}</p>
139+
<Icon
140+
type="md"
141+
className="size-[14px]"
142+
/>
143+
</div>
144+
</DropdownMenuItem>
145+
<DropdownMenuItem onClick={() => create("code")}>
146+
<div className="flex flex-row items-center gap-8 w-full justify-between">
147+
<p>{t("notes.menu.typeCode")}</p>
148+
<Icon
149+
type="code"
150+
className="size-[14px]"
151+
/>
152+
</div>
153+
</DropdownMenuItem>
154+
</DropdownMenuContent>
155+
</DropdownMenu>
156+
{notes.length > 0 && (
157+
<Button
158+
onClick={openLatest}
159+
size="sm"
160+
variant="link"
161+
className="text-muted-foreground text-xs"
162+
>
163+
tbd
164+
</Button>
165+
)}
166+
</div>
167+
</div>
168+
)
169+
})
170+
171+
Empty.displayName = "Empty"
172+
173+
export default Empty

0 commit comments

Comments
 (0)