Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
105 changes: 99 additions & 6 deletions src/app/document/[documentId]/(navbar)/document-input.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,105 @@
import { CloudFogIcon } from "lucide-react";
import { useDebounce } from "@/hooks/use-debounce";
import { useToast } from "@/hooks/use-toast";
import { useStatus } from "@liveblocks/react";
import { useMutation } from "convex/react";
import { CloudAlertIcon, CloudIcon, CloudUploadIcon } from "lucide-react";
import React, { useRef, useState } from "react";
import { api } from "../../../../../convex/_generated/api";
import { Id } from "../../../../../convex/_generated/dataModel";

type Props = {
id: Id<"documents">;
title: string;
};

export const DocumentInput: React.FC<Props> = ({ id, title }) => {
const [value, setValue] = useState<string>(title);
const [isPending, setIsPending] = useState<boolean>(false);
const [isEditing, setIsEditing] = useState<boolean>(false);
const inputRef = useRef<HTMLInputElement>(null);
const mutate = useMutation(api.document.update);
const status = useStatus();

const { toast } = useToast();

const update = (value: string) => {
setIsPending(true);
mutate({
documentId: id,
title: value,
})
.then(() => {
toast({
title: "Document updated",
description: "Your document has been updated.",
});
})
.catch(() => {
toast({
title: "Document update failed",
description: "Your document could not be updated",
variant: "destructive",
});
})
.finally(() => {
setIsPending(false);
});
};

const debounce = useDebounce((value: string) => {
if (value === title) return;
update(value);
}, 1000);

const submit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
update(value);
};

const onChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setValue(e.target.value);
debounce(e.target.value);
};

const isCloudLoading =
isPending || status === "connecting" || status === "reconnecting";
const isCloudError = status === "disconnected";

export const DocumentInput: React.FC = () => {
return (
<div className="flex items-center gap-2">
<span className="text-lg px-1.5 cursor-pointer truncate">
Untitled Document
</span>
<CloudFogIcon />
{isEditing ? (
<>
<form className="relative w-fit max-w-[50ch]" onSubmit={submit}>
<span className="invisible whitespace-pre px-1.5 text-lg">
{value || " "}
</span>
<input
ref={inputRef}
value={value}
onChange={onChange}
onBlur={() => setIsEditing(false)}
className="absolute inset-0 text-lg text-black px-1.5 bg-transparent truncate "
/>
</form>
</>
) : (
<>
<span
className="text-lg px-1.5 cursor-pointer truncate"
onClick={() => {
setIsEditing(true);
setTimeout(() => {
inputRef.current?.focus();
}, 0);
}}
>
{title}
</span>
</>
)}
{!isCloudError && !isCloudLoading && <CloudIcon />}
{isCloudLoading && <CloudUploadIcon />}
{isCloudError && <CloudAlertIcon />}
</div>
);
};
13 changes: 9 additions & 4 deletions src/app/document/[documentId]/(navbar)/menu-bar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,13 @@ import {
UnderlineIcon,
Undo2Icon,
} from "lucide-react";
import { Doc } from "../../../../../convex/_generated/dataModel";

export const MenuBar: React.FC = () => {
type Props = {
data: Doc<"documents">;
};

export const MenuBar: React.FC<Props> = ({ data }) => {
const { editor } = useEditorStore();

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

onDownload(blob, "document.json");
onDownload(blob, `${data.title}.json`);
};

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

onDownload(blob, "document.html");
onDownload(blob, `${data.title}.html`);
};

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

onDownload(blob, "document.txt");
onDownload(blob, `${data.title}.txt`);
};

return (
Expand Down
11 changes: 8 additions & 3 deletions src/app/document/[documentId]/(navbar)/navbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,17 @@ import { SearchInput } from "@/app/(home)/SearchInput";
import { OrganizationSwitcher, UserButton } from "@clerk/clerk-react";
import Image from "next/image";
import Link from "next/link";
import { Doc } from "../../../../../convex/_generated/dataModel";
import { Avatars } from "../avatars";
import { Inbox } from "../inbox";
import { DocumentInput } from "./document-input";
import { MenuBar } from "./menu-bar";

export const Navbar: React.FC = () => {
type Props = {
document: Doc<"documents">;
};

export const Navbar: React.FC<Props> = ({ document }) => {
return (
<>
<nav className="flex items-center justify-between bg-white">
Expand All @@ -23,8 +28,8 @@ export const Navbar: React.FC = () => {
/>
</Link>
<div className="flex flex-col">
<DocumentInput />
<MenuBar />
<DocumentInput title={document.title} id={document._id} />
<MenuBar data={document} />
</div>
</div>

Expand Down
29 changes: 29 additions & 0 deletions src/app/document/[documentId]/document.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
"use client";

import { Preloaded, usePreloadedQuery } from "convex/react";
import { Navbar } from "./(navbar)/navbar";
import { Toolbar } from "./(toolbar)/toolbar";
import { Editor } from "./editor";
import { Room } from "./room";
import { api } from "../../../../convex/_generated/api";

interface Props {
preloadedDocument: Preloaded<typeof api.document.get>;
}
export const Document: React.FC<Props> = ({ preloadedDocument }) => {
const document = usePreloadedQuery(preloadedDocument);
return (
<Room>
<div className="min-h-screen bg-[#fafbfd]">
<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">
<Navbar document={document} />
<Toolbar />
</div>

<div className="pt-[114px] print:pt-0">
<Editor />
</div>
</div>
</Room>
);
};
11 changes: 11 additions & 0 deletions src/app/document/[documentId]/loading.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { FullscreenLoader } from "@/components/fullscreen-loader";

const Page: React.FC = () => {
return (
<>
<FullscreenLoader label="Document loading..." />
</>
);
};

export default Page;
37 changes: 19 additions & 18 deletions src/app/document/[documentId]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,29 +1,30 @@
import { Navbar } from "./(navbar)/navbar";
import { Toolbar } from "./(toolbar)/toolbar";
import { Editor } from "./editor";
import { Room } from "./room";
import { preloadQuery } from "convex/nextjs";
import { auth } from "@clerk/nextjs/server";
import { Id } from "../../../../convex/_generated/dataModel";
import { Document } from "./document";
import { api } from "../../../../convex/_generated/api";

interface Props {
params: Promise<{ documentId: string }>;
params: Promise<{ documentId: Id<"documents"> }>;
}
const Page: React.FC<Props> = async ({ params }) => {
const { documentId } = await params;
console.info(":Document:", documentId);
const { getToken } = await auth();
const token = (await getToken({ template: "convex" })) ?? "";

return (
<Room>
<div className="min-h-screen bg-[#fafbfd]">
<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">
<Navbar />
<Toolbar />
</div>
if (!token) throw new Error("Unauthorized");

<div className="pt-[114px] print:pt-0">
<Editor />
</div>
</div>
</Room>
const preloadedDocument = await preloadQuery(
api.document.get,
{
id: documentId,
},
{
token,
}
);

return <Document preloadedDocument={preloadedDocument} />;
};

export default Page;
2 changes: 1 addition & 1 deletion src/app/document/[documentId]/room.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ export function Room({ children }: { children: ReactNode }) {
}}
>
<ClientSideSuspense
fallback={<FullscreenLoader label="Document loading..." />}
fallback={<FullscreenLoader label="Workspace loading..." />}
>
{children}
</ClientSideSuspense>
Expand Down
23 changes: 23 additions & 0 deletions src/hooks/use-debounce.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { useCallback, useRef } from "react";

export const useDebounce = <
T extends (...args: Parameters<T>) => ReturnType<T>,
>(
callback: T,
delay: number = 500
) => {
const timeoutRef = useRef<NodeJS.Timeout>(null);

return useCallback(
(...args: Parameters<T>) => {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
}

timeoutRef.current = setTimeout(() => {
callback(...args);
}, delay);
},
[callback, delay]
);
};