Skip to content

Commit f727f34

Browse files
committed
add: notion style editor, not-found.tsx page
1 parent 24d518f commit f727f34

File tree

14 files changed

+3867
-219
lines changed

14 files changed

+3867
-219
lines changed

.lintstagedrc.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
// .lintstagedrc.js
22
module.exports = {
3-
"src/**/*.js": ["npm run lint:js"],
3+
"app/**/*.js": ["npm run lint:js"],
4+
"app/**/*.ts": ["npm run lint:js"],
45
};

.vscode/settings.json

+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
{
2+
"workbench.colorCustomizations": {
3+
"activityBar.activeBackground": "#b4e900",
4+
"activityBar.background": "#b4e900",
5+
"activityBar.foreground": "#15202b",
6+
"activityBar.inactiveForeground": "#15202b99",
7+
"activityBarBadge.background": "#0094bf",
8+
"activityBarBadge.foreground": "#e7e7e7",
9+
"commandCenter.border": "#15202b99",
10+
"sash.hoverBorder": "#b4e900",
11+
"statusBar.background": "#8db600",
12+
"statusBar.foreground": "#15202b",
13+
"statusBarItem.hoverBackground": "#658300",
14+
"statusBarItem.remoteBackground": "#8db600",
15+
"statusBarItem.remoteForeground": "#15202b",
16+
"titleBar.activeBackground": "#8db600",
17+
"titleBar.activeForeground": "#15202b",
18+
"titleBar.inactiveBackground": "#8db60099",
19+
"titleBar.inactiveForeground": "#15202b99"
20+
},
21+
"peacock.color": "#8DB600"
22+
}

app/(main)/(routes)/documents/[documentId]/page.tsx

+28-2
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
"use client";
22

3-
import { useQuery } from "convex/react";
3+
import dynamic from "next/dynamic";
4+
import { useMemo } from "react";
5+
import { useMutation, useQuery } from "convex/react";
46

57
import { Cover } from "@/components/cover";
68
import { Id } from "@/convex/_generated/dataModel";
79
import { api } from "@/convex/_generated/api";
810
import { Toolbar } from "@/components/toolbar";
11+
import { Skeleton } from "@/components/ui/skeleton";
912

1013
interface DocumentIdPageProps {
1114
params: {
@@ -17,12 +20,34 @@ interface DocumentIdPageProps {
1720
* @description page to render for url `/documents/${documentId}`
1821
*/
1922
const DocumentIdPage = ({ params }: DocumentIdPageProps) => {
23+
const Editor = useMemo(
24+
() => dynamic(() => import("@/components/editor"), { ssr: false }),
25+
[]
26+
);
27+
2028
const document = useQuery(api.documents.getDocumentById, {
2129
documentId: params.documentId,
2230
});
2331

32+
const update = useMutation(api.documents.updateDocuments);
33+
34+
const handleUpdate = (content: string) => {
35+
update({ id: params.documentId, content });
36+
};
2437
if (document === undefined) {
25-
return <div>loading</div>;
38+
return (
39+
<div>
40+
<Cover.Skeleton />
41+
<div className="md:max-w-3xl lg:max-w-4xl mx-auto mt-10">
42+
<div className="space-y-4 pl-8 pt-4">
43+
<Skeleton className="h-14 w-[50%]" />
44+
<Skeleton className="h-4 w-[80%]" />
45+
<Skeleton className="h-4 w-[40%]" />
46+
<Skeleton className="h-4 w-[60%]" />
47+
</div>
48+
</div>
49+
</div>
50+
);
2651
}
2752

2853
if (document === null) {
@@ -35,6 +60,7 @@ const DocumentIdPage = ({ params }: DocumentIdPageProps) => {
3560
<Cover url={document.coverImage} />
3661
<div className="md:max-w-3xl lg:max-w-4xl mx-auto">
3762
<Toolbar initialData={document} />
63+
<Editor onChange={handleUpdate} initialContent={document.content} />
3864
</div>
3965
</div>
4066
</>

app/(main)/(routes)/documents/page.tsx

+6-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
"use client";
22

33
import Image from "next/image";
4+
import { useRouter } from "next/navigation";
45
import React from "react";
56
import { LucidePlusCircle } from "lucide-react";
67
import { useUser } from "@clerk/clerk-react";
@@ -11,10 +12,14 @@ import { Button } from "@/components/ui/button";
1112
import { api } from "@/convex/_generated/api";
1213

1314
const Documents = () => {
15+
const router = useRouter();
1416
const { user } = useUser();
1517
const createNote = useMutation(api.documents.createNote); //call createNote fn and pass it to useMutation hook to create new note
18+
1619
const handleCreateNote = () => {
17-
const promise = createNote({ title: "Untitled" });
20+
const promise = createNote({ title: "Untitled" }).then((documentId) =>
21+
router.push(`/documents/${documentId}`)
22+
);
1823

1924
toast.promise(promise, {
2025
loading: "Creating note...",

app/(main)/_components/item.tsx

+10-2
Original file line numberDiff line numberDiff line change
@@ -53,10 +53,10 @@ export const Item = ({
5353
}: ItemProps) => {
5454
const ChevronIcon = expanded ? ChevronDown : ChevronRight;
5555

56+
const router = useRouter();
5657
const user = useUser();
5758

5859
const create = useMutation(api.documents.createNote);
59-
6060
const archive = useMutation(api.documents.archiveDocuments);
6161

6262
/**
@@ -75,6 +75,11 @@ export const Item = ({
7575
const promise = create({
7676
title: "Untitled",
7777
parentDocument: id,
78+
}).then((documentId) => {
79+
if (!expanded) {
80+
onExpand?.();
81+
}
82+
router.push(`/documents/${documentId}`);
7883
});
7984

8085
toast.promise(promise, {
@@ -88,7 +93,10 @@ export const Item = ({
8893
if (!id) {
8994
return;
9095
}
91-
const promise = archive({ id });
96+
97+
const promise = archive({ id }).then((documentId) =>
98+
router.push("/documents")
99+
);
92100

93101
toast.promise(promise, {
94102
loading: "Deleting note...",

app/(main)/_components/navigation.tsx

+5-3
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import {
77
Settings2Icon,
88
Trash2Icon,
99
} from "lucide-react";
10-
import { usePathname, useParams } from "next/navigation";
10+
import { usePathname, useParams, useRouter } from "next/navigation";
1111
import React, { ElementRef, useEffect, useRef, useState } from "react";
1212
import { useMediaQuery } from "usehooks-ts";
1313
import { useMutation } from "convex/react";
@@ -30,7 +30,7 @@ import { Navbar } from "./navbar";
3030

3131
const Navigation = () => {
3232
//hooks to interact with app
33-
33+
const router = useRouter();
3434
const settings = useSettings();
3535
const search = useSearch();
3636
const params = useParams();
@@ -155,7 +155,9 @@ const Navigation = () => {
155155
}, [pathname, isMobile]);
156156

157157
const handleCreateNote = () => {
158-
const promise = createNote({ title: "Untitled" });
158+
const promise = createNote({ title: "Untitled" }).then((documentId) =>
159+
router.push(`/documents/${documentId}`)
160+
);
159161

160162
toast.promise(promise, {
161163
loading: "Creating note...",

app/api/edgestore/[...edgestore]/route.ts

+6-1
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,18 @@ const es = initEdgeStore.create();
55
* This is the main router for the Edge Store buckets.
66
*/
77
const edgeStoreRouter = es.router({
8-
publicFiles: es.fileBucket(),
8+
publicFiles: es.fileBucket().beforeDelete(() => {
9+
return true; // allow delete
10+
}),
911
});
12+
1013
const handler = createEdgeStoreNextHandler({
1114
router: edgeStoreRouter,
1215
});
16+
1317
export { handler as GET, handler as POST };
1418
/**
1519
* This type is used to create the type-safe client for the frontend.
1620
*/
21+
1722
export type EdgeStoreRouter = typeof edgeStoreRouter;

app/not-found.tsx

+34
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import Image from "next/image";
2+
import Link from "next/link";
3+
4+
import { Button } from "@/components/ui/button";
5+
6+
export default function NotFound() {
7+
return (
8+
<>
9+
<div className="h-full flex flex-col items-center justify-center space-y-4">
10+
<Image
11+
src="/images/error-light.png"
12+
alt="error"
13+
height="300"
14+
width="300"
15+
className="dark:hidden"
16+
/>
17+
<Image
18+
src="/images/error-dark.png"
19+
alt="error"
20+
height="300"
21+
width="300"
22+
className="hidden dark:block"
23+
/>{" "}
24+
<h2 className="text-xl font-medium text-center justify-center flex">
25+
Something went wrong!
26+
</h2>
27+
<Button asChild>
28+
<Link href="/documents" />
29+
Back to homepage
30+
</Button>
31+
</div>
32+
</>
33+
);
34+
}

components/cover.tsx

+15-2
Original file line numberDiff line numberDiff line change
@@ -10,19 +10,28 @@ import { Button } from "@/components/ui/button";
1010
import { useCoverImage } from "@/hooks/use-cover-image";
1111
import { Id } from "@/convex/_generated/dataModel";
1212
import { api } from "@/convex/_generated/api";
13+
import { useEdgeStore } from "@/lib/edgestore";
14+
import { Skeleton } from "@/components/ui/skeleton";
1315

1416
interface CoverImageProps {
1517
url?: string;
1618
preview?: boolean;
1719
}
1820

1921
export const Cover = ({ preview, url }: CoverImageProps) => {
22+
const { edgestore } = useEdgeStore();
23+
2024
const coverImage = useCoverImage();
2125
const params = useParams();
2226

2327
const removeCoverImage = useMutation(api.documents.removeCoverImage);
2428

25-
const handleRemoveCoverImage = () => {
29+
const handleRemoveCoverImage = async () => {
30+
if (url) {
31+
await edgestore.publicFiles.delete({
32+
url: url!,
33+
});
34+
}
2635
removeCoverImage({ id: params.documentId as Id<"documents"> });
2736
};
2837

@@ -39,7 +48,7 @@ export const Cover = ({ preview, url }: CoverImageProps) => {
3948
<div className="opacity-0 group-hover:opacity-100 absolute bottom-5 right-5 flex items-center gap-x-2">
4049
<Button
4150
type="button"
42-
onClick={coverImage.onOpen}
51+
onClick={() => coverImage.onReplace(url)}
4352
variant="outline"
4453
size="sm"
4554
className="text-muted-foreground text-xs"
@@ -62,3 +71,7 @@ export const Cover = ({ preview, url }: CoverImageProps) => {
6271
</div>
6372
);
6473
};
74+
75+
Cover.Skeleton = function CoverSkeleton() {
76+
return <Skeleton className="w-full h-[12vh]" />;
77+
};

components/editor.tsx

+72
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
"use client";
2+
3+
import {
4+
BlockConfig,
5+
BlockNoteEditor,
6+
BlockSchemaFromSpecs,
7+
BlockSpecs,
8+
InlineContentSchema,
9+
InlineContentSchemaFromSpecs,
10+
InlineContentSpecs,
11+
PartialBlock,
12+
StyleSchema,
13+
StyleSchemaFromSpecs,
14+
StyleSpecs,
15+
} from "@blocknote/core";
16+
import { BlockNoteView, useBlockNote } from "@blocknote/react";
17+
import "@blocknote/core/style.css";
18+
import { useEdgeStore } from "@/lib/edgestore";
19+
import { useTheme } from "next-themes";
20+
21+
interface EditorProps {
22+
onChange: (value: string) => void;
23+
initialContent?: string;
24+
editable?: boolean;
25+
}
26+
27+
type BlockConfigMap = Record<string, BlockConfig>;
28+
29+
const Editor = ({ onChange, initialContent, editable }: EditorProps) => {
30+
const { edgestore } = useEdgeStore();
31+
const { resolvedTheme } = useTheme();
32+
33+
const handleUpload = async (file: File) => {
34+
const response = await edgestore.publicFiles.upload({
35+
file,
36+
});
37+
38+
return response.url;
39+
};
40+
41+
const editor: BlockNoteEditor<
42+
BlockSchemaFromSpecs<BlockSpecs>,
43+
InlineContentSchemaFromSpecs<InlineContentSpecs>,
44+
StyleSchemaFromSpecs<StyleSpecs>
45+
> = useBlockNote({
46+
editable,
47+
initialContent: initialContent
48+
? (JSON.parse(initialContent) as PartialBlock<
49+
BlockConfigMap,
50+
InlineContentSchema,
51+
StyleSchema
52+
>[])
53+
: undefined,
54+
onEditorContentChange: (editor) => {
55+
onChange(JSON.stringify(editor.topLevelBlocks, null, 2));
56+
},
57+
uploadFile: handleUpload,
58+
});
59+
60+
return (
61+
<>
62+
<div>
63+
<BlockNoteView
64+
editor={editor}
65+
theme={resolvedTheme === "dark" ? "dark" : "light"}
66+
/>
67+
</div>
68+
</>
69+
);
70+
};
71+
72+
export default Editor;

components/modals/cover-image-modal.tsx

+3
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,9 @@ export const CoverImageModal = () => {
3333

3434
const res = await edgestore.publicFiles.upload({
3535
file,
36+
options: {
37+
replaceTargetUrl: coverImage.url,
38+
},
3639
});
3740

3841
await update({

hooks/use-cover-image.tsx

+6-2
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,20 @@
11
import { create } from "zustand";
22

33
type CoverImageStore = {
4+
url?: string;
45
isOpen: boolean;
56
onOpen: () => void;
67
onClose: () => void;
8+
onReplace: (url: string) => void;
79
};
810

911
/**
1012
* @description hook to open/close cover image modal
1113
*/
1214
export const useCoverImage = create<CoverImageStore>((set) => ({
15+
url: undefined,
1316
isOpen: false,
14-
onOpen: () => set({ isOpen: true }),
15-
onClose: () => set({ isOpen: false }),
17+
onOpen: () => set({ isOpen: true, url: undefined }),
18+
onClose: () => set({ isOpen: false, url: undefined }),
19+
onReplace: (url: string) => set({ isOpen: true, url }),
1620
}));

0 commit comments

Comments
 (0)