Skip to content

Commit 7b7c88c

Browse files
authored
Merge pull request #38 from duylongpro99/development
preload document, load document action, update document title
2 parents 61e494d + 471c2c8 commit 7b7c88c

File tree

8 files changed

+199
-32
lines changed

8 files changed

+199
-32
lines changed
Lines changed: 99 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,105 @@
1-
import { CloudFogIcon } from "lucide-react";
1+
import { useDebounce } from "@/hooks/use-debounce";
2+
import { useToast } from "@/hooks/use-toast";
3+
import { useStatus } from "@liveblocks/react";
4+
import { useMutation } from "convex/react";
5+
import { CloudAlertIcon, CloudIcon, CloudUploadIcon } from "lucide-react";
6+
import React, { useRef, useState } from "react";
7+
import { api } from "../../../../../convex/_generated/api";
8+
import { Id } from "../../../../../convex/_generated/dataModel";
9+
10+
type Props = {
11+
id: Id<"documents">;
12+
title: string;
13+
};
14+
15+
export const DocumentInput: React.FC<Props> = ({ id, title }) => {
16+
const [value, setValue] = useState<string>(title);
17+
const [isPending, setIsPending] = useState<boolean>(false);
18+
const [isEditing, setIsEditing] = useState<boolean>(false);
19+
const inputRef = useRef<HTMLInputElement>(null);
20+
const mutate = useMutation(api.document.update);
21+
const status = useStatus();
22+
23+
const { toast } = useToast();
24+
25+
const update = (value: string) => {
26+
setIsPending(true);
27+
mutate({
28+
documentId: id,
29+
title: value,
30+
})
31+
.then(() => {
32+
toast({
33+
title: "Document updated",
34+
description: "Your document has been updated.",
35+
});
36+
})
37+
.catch(() => {
38+
toast({
39+
title: "Document update failed",
40+
description: "Your document could not be updated",
41+
variant: "destructive",
42+
});
43+
})
44+
.finally(() => {
45+
setIsPending(false);
46+
});
47+
};
48+
49+
const debounce = useDebounce((value: string) => {
50+
if (value === title) return;
51+
update(value);
52+
}, 1000);
53+
54+
const submit = (e: React.FormEvent<HTMLFormElement>) => {
55+
e.preventDefault();
56+
update(value);
57+
};
58+
59+
const onChange = (e: React.ChangeEvent<HTMLInputElement>) => {
60+
setValue(e.target.value);
61+
debounce(e.target.value);
62+
};
63+
64+
const isCloudLoading =
65+
isPending || status === "connecting" || status === "reconnecting";
66+
const isCloudError = status === "disconnected";
267

3-
export const DocumentInput: React.FC = () => {
468
return (
569
<div className="flex items-center gap-2">
6-
<span className="text-lg px-1.5 cursor-pointer truncate">
7-
Untitled Document
8-
</span>
9-
<CloudFogIcon />
70+
{isEditing ? (
71+
<>
72+
<form className="relative w-fit max-w-[50ch]" onSubmit={submit}>
73+
<span className="invisible whitespace-pre px-1.5 text-lg">
74+
{value || " "}
75+
</span>
76+
<input
77+
ref={inputRef}
78+
value={value}
79+
onChange={onChange}
80+
onBlur={() => setIsEditing(false)}
81+
className="absolute inset-0 text-lg text-black px-1.5 bg-transparent truncate "
82+
/>
83+
</form>
84+
</>
85+
) : (
86+
<>
87+
<span
88+
className="text-lg px-1.5 cursor-pointer truncate"
89+
onClick={() => {
90+
setIsEditing(true);
91+
setTimeout(() => {
92+
inputRef.current?.focus();
93+
}, 0);
94+
}}
95+
>
96+
{title}
97+
</span>
98+
</>
99+
)}
100+
{!isCloudError && !isCloudLoading && <CloudIcon />}
101+
{isCloudLoading && <CloudUploadIcon />}
102+
{isCloudError && <CloudAlertIcon />}
10103
</div>
11104
);
12105
};

src/app/document/[documentId]/(navbar)/menu-bar.tsx

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,13 @@ import {
3131
UnderlineIcon,
3232
Undo2Icon,
3333
} from "lucide-react";
34+
import { Doc } from "../../../../../convex/_generated/dataModel";
3435

35-
export const MenuBar: React.FC = () => {
36+
type Props = {
37+
data: Doc<"documents">;
38+
};
39+
40+
export const MenuBar: React.FC<Props> = ({ data }) => {
3641
const { editor } = useEditorStore();
3742

3843
const addTable = ({ rows, cols }: { rows: number; cols: number }) => {
@@ -63,7 +68,7 @@ export const MenuBar: React.FC = () => {
6368
type: "application/json",
6469
});
6570

66-
onDownload(blob, "document.json");
71+
onDownload(blob, `${data.title}.json`);
6772
};
6873

6974
const onSaveHTML = () => {
@@ -74,7 +79,7 @@ export const MenuBar: React.FC = () => {
7479
type: "text/html",
7580
});
7681

77-
onDownload(blob, "document.html");
82+
onDownload(blob, `${data.title}.html`);
7883
};
7984

8085
const onSaveText = () => {
@@ -85,7 +90,7 @@ export const MenuBar: React.FC = () => {
8590
type: "text/plain",
8691
});
8792

88-
onDownload(blob, "document.txt");
93+
onDownload(blob, `${data.title}.txt`);
8994
};
9095

9196
return (

src/app/document/[documentId]/(navbar)/navbar.tsx

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,17 @@ import { SearchInput } from "@/app/(home)/SearchInput";
44
import { OrganizationSwitcher, UserButton } from "@clerk/clerk-react";
55
import Image from "next/image";
66
import Link from "next/link";
7+
import { Doc } from "../../../../../convex/_generated/dataModel";
78
import { Avatars } from "../avatars";
89
import { Inbox } from "../inbox";
910
import { DocumentInput } from "./document-input";
1011
import { MenuBar } from "./menu-bar";
1112

12-
export const Navbar: React.FC = () => {
13+
type Props = {
14+
document: Doc<"documents">;
15+
};
16+
17+
export const Navbar: React.FC<Props> = ({ document }) => {
1318
return (
1419
<>
1520
<nav className="flex items-center justify-between bg-white">
@@ -23,8 +28,8 @@ export const Navbar: React.FC = () => {
2328
/>
2429
</Link>
2530
<div className="flex flex-col">
26-
<DocumentInput />
27-
<MenuBar />
31+
<DocumentInput title={document.title} id={document._id} />
32+
<MenuBar data={document} />
2833
</div>
2934
</div>
3035

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
"use client";
2+
3+
import { Preloaded, usePreloadedQuery } from "convex/react";
4+
import { Navbar } from "./(navbar)/navbar";
5+
import { Toolbar } from "./(toolbar)/toolbar";
6+
import { Editor } from "./editor";
7+
import { Room } from "./room";
8+
import { api } from "../../../../convex/_generated/api";
9+
10+
interface Props {
11+
preloadedDocument: Preloaded<typeof api.document.get>;
12+
}
13+
export const Document: React.FC<Props> = ({ preloadedDocument }) => {
14+
const document = usePreloadedQuery(preloadedDocument);
15+
return (
16+
<Room>
17+
<div className="min-h-screen bg-[#fafbfd]">
18+
<div className="flex flex-col px-4 pt-2 gap-y-2 fixed top-0 left-0 right-0 z-10 bg-[#fafbfd] print:hidden">
19+
<Navbar document={document} />
20+
<Toolbar />
21+
</div>
22+
23+
<div className="pt-[114px] print:pt-0">
24+
<Editor />
25+
</div>
26+
</div>
27+
</Room>
28+
);
29+
};
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { FullscreenLoader } from "@/components/fullscreen-loader";
2+
3+
const Page: React.FC = () => {
4+
return (
5+
<>
6+
<FullscreenLoader label="Document loading..." />
7+
</>
8+
);
9+
};
10+
11+
export default Page;
Lines changed: 19 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,30 @@
1-
import { Navbar } from "./(navbar)/navbar";
2-
import { Toolbar } from "./(toolbar)/toolbar";
3-
import { Editor } from "./editor";
4-
import { Room } from "./room";
1+
import { preloadQuery } from "convex/nextjs";
2+
import { auth } from "@clerk/nextjs/server";
3+
import { Id } from "../../../../convex/_generated/dataModel";
4+
import { Document } from "./document";
5+
import { api } from "../../../../convex/_generated/api";
56

67
interface Props {
7-
params: Promise<{ documentId: string }>;
8+
params: Promise<{ documentId: Id<"documents"> }>;
89
}
910
const Page: React.FC<Props> = async ({ params }) => {
1011
const { documentId } = await params;
11-
console.info(":Document:", documentId);
12+
const { getToken } = await auth();
13+
const token = (await getToken({ template: "convex" })) ?? "";
1214

13-
return (
14-
<Room>
15-
<div className="min-h-screen bg-[#fafbfd]">
16-
<div className="flex flex-col px-4 pt-2 gap-y-2 fixed top-0 left-0 right-0 z-10 bg-[#fafbfd] print:hidden">
17-
<Navbar />
18-
<Toolbar />
19-
</div>
15+
if (!token) throw new Error("Unauthorized");
2016

21-
<div className="pt-[114px] print:pt-0">
22-
<Editor />
23-
</div>
24-
</div>
25-
</Room>
17+
const preloadedDocument = await preloadQuery(
18+
api.document.get,
19+
{
20+
id: documentId,
21+
},
22+
{
23+
token,
24+
}
2625
);
26+
27+
return <Document preloadedDocument={preloadedDocument} />;
2728
};
2829

2930
export default Page;

src/app/document/[documentId]/room.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ export function Room({ children }: { children: ReactNode }) {
8787
}}
8888
>
8989
<ClientSideSuspense
90-
fallback={<FullscreenLoader label="Document loading..." />}
90+
fallback={<FullscreenLoader label="Workspace loading..." />}
9191
>
9292
{children}
9393
</ClientSideSuspense>

src/hooks/use-debounce.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { useCallback, useRef } from "react";
2+
3+
export const useDebounce = <
4+
T extends (...args: Parameters<T>) => ReturnType<T>,
5+
>(
6+
callback: T,
7+
delay: number = 500
8+
) => {
9+
const timeoutRef = useRef<NodeJS.Timeout>(null);
10+
11+
return useCallback(
12+
(...args: Parameters<T>) => {
13+
if (timeoutRef.current) {
14+
clearTimeout(timeoutRef.current);
15+
}
16+
17+
timeoutRef.current = setTimeout(() => {
18+
callback(...args);
19+
}, delay);
20+
},
21+
[callback, delay]
22+
);
23+
};

0 commit comments

Comments
 (0)