diff --git a/app/components/Card/Card.tsx b/app/components/Card/Card.tsx index 91f3a19..0c60c7f 100644 --- a/app/components/Card/Card.tsx +++ b/app/components/Card/Card.tsx @@ -2,7 +2,6 @@ import { useRef, useState } from "react"; import { useRouter } from "next/navigation"; -import { CircleCheck } from "lucide-react"; import { Card, CardContent, CardFooter } from "@/components/ui/card"; import { Input } from "@/components/ui/input"; @@ -13,6 +12,7 @@ import AvatarList from "@/components/AvatarList"; import CardOptions from "./components/Options"; import prettifyDate from "@/helpers/prettifyDates"; import useClientSession from "@/lib/customHooks/useClientSession"; +import useDebounce from "@/lib/customHooks/useDebounce"; type DocCardPropType = { docId: string; @@ -32,6 +32,11 @@ export default function DocCard({ }: DocCardPropType) { const router = useRouter(); + const debounce = useDebounce(async () => { + if (!inputRef.current || !session?.id) return; + await RenameDocument(docId, inputRef.current.value); + }, 1000) + const session = useClientSession(); localStorage.setItem("name", session.name as string); @@ -52,15 +57,11 @@ export default function DocCard({ ref={inputRef} value={name} className="w-full text-md border-none focus-visible:bg-slate-50" - onChange={(e) => setName(e.target.value)} - /> - { - if (!inputRef.current || !session?.id) return; - await RenameDocument(docId, inputRef.current.value); + onChange={(e) => { + setName(e.target.value); + debounce(e.target.value); }} - className={`${title != name ? "" : "hidden"} size-4 text-slate-500 hover:cursor-pointer absolute right-3 top-1/2 transform -translate-y-1/2`} - > + />
diff --git a/app/components/Card/components/Options.tsx b/app/components/Card/components/Options.tsx index eb48258..7aead64 100644 --- a/app/components/Card/components/Options.tsx +++ b/app/components/Card/components/Options.tsx @@ -25,7 +25,6 @@ import { import { Button } from "@/components/ui/button"; import { DeleteDocument } from "../actions"; -import useClientSession from "@/lib/customHooks/useClientSession"; import LoaderButton from "@/components/LoaderButton"; type CardOptionsPropType = { diff --git a/app/components/Header/Header.tsx b/app/components/Header/Header.tsx index 5cc78bb..679d44e 100644 --- a/app/components/Header/Header.tsx +++ b/app/components/Header/Header.tsx @@ -2,10 +2,8 @@ import Image from "next/image"; import { Montserrat_Alternates as Montserrat } from "next/font/google" import { SessionReturnType } from "@/lib/customHooks/ReturnType"; -import getInitials from "@/helpers/getInitials"; import logo from "@/public/logo.svg"; -import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; import SearchBar from "./components/SearchBar"; import HeaderButtons from "./components/HeaderButtons"; @@ -18,25 +16,14 @@ const roboto = Montserrat({ type HeaderPropType = Pick; export default function Header({ image, name }: HeaderPropType) { return ( -
+
logo -

DocX

+

DocX

{/* logo */}
- - {process.env.NODE_ENV === "production" ? ( - <> - ) : ( - - {image ? ( - - ) : ( - {getInitials(name ?? "")} - )} - - )} +
); } diff --git a/app/components/Header/components/HeaderButtons.tsx b/app/components/Header/components/HeaderButtons.tsx index 1e369b7..c15c16c 100644 --- a/app/components/Header/components/HeaderButtons.tsx +++ b/app/components/Header/components/HeaderButtons.tsx @@ -5,22 +5,24 @@ import { useRouter } from "next/navigation"; import { toast } from "sonner"; import { CloudUpload, LogOut, PlusIcon } from "lucide-react"; -import { Button } from "@/components/ui/button"; +import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; import { CreateNewDocument, LogoutAction } from "../actions"; -import useClientSession from "@/lib/customHooks/useClientSession"; +import { Button } from "@/components/ui/button"; +import { SessionReturnType } from "@/lib/customHooks/ReturnType"; import LoaderButton from "@/components/LoaderButton"; +import getInitials from "@/helpers/getInitials"; +import { Popover } from "@/components/ui/popover"; +import { PopoverContent, PopoverTrigger } from "@radix-ui/react-popover"; -export default function HeaderButtons() { +type HeaderBtnPropType = Pick; +export default function HeaderButtons({ image, name }: HeaderBtnPropType) { const router = useRouter(); - const session = useClientSession(); - const [isLoading, setIsLoading] = useState(false); const createDocument = async () => { setIsLoading(true); - if (!session?.id) return; const response = await CreateNewDocument(); if (response.success) { setIsLoading(false); @@ -34,7 +36,6 @@ export default function HeaderButtons() { const logout = async () => { const response = await LogoutAction(); - console.log(response); if (response.success) { toast.success("Successfully logged out"); router.push("/api/auth/signin"); @@ -45,13 +46,16 @@ export default function HeaderButtons() { return (
- + {process.env.NODE_ENV === "development" ? + + : <> + } Create New

- -
+ + +
+

{getInitials(name ?? "X")}

+
+ {image ? ( + + + + ) : (<>)} +
+ + + +
+
); } diff --git a/app/components/Header/components/SearchBar.tsx b/app/components/Header/components/SearchBar.tsx index 1a0b6fa..04054c6 100644 --- a/app/components/Header/components/SearchBar.tsx +++ b/app/components/Header/components/SearchBar.tsx @@ -1,14 +1,14 @@ "use client"; -import { useCallback, useEffect, useMemo, useRef, useState } from "react"; +import { useEffect, useRef, useState } from "react"; import { useRouter } from "next/navigation"; import { Search, X } from "lucide-react"; -import { debounce } from "lodash"; import Image from "next/image"; import { Input } from "@/components/ui/input"; import prettifyDate from "@/helpers/prettifyDates"; import doc from "@/public/output-onlinepngtools.svg"; +import useDebounce from "@/lib/customHooks/useDebounce"; import { SearchDocAction } from "../actions"; @@ -28,6 +28,14 @@ type SearchResponse = { export default function SearchBar() { const router = useRouter(); + const debounce = useDebounce( + async (value: string) => { + if (!searchValue) return; + setIsSearching(true); + setSearchResponse(await SearchDocAction(value)); + setIsSearching(false); + }, 500); + const searchedResponseRef = useRef(null); const [searchResponse, setSearchResponse] = useState< @@ -37,21 +45,6 @@ export default function SearchBar() { const [isFocused, setIsFocused] = useState(false); const [isSearching, setIsSearching] = useState(false); - const search = useCallback( - async (value: string) => { - if (!searchValue) return; - setIsSearching(true); - setSearchResponse(await SearchDocAction(value)); - setIsSearching(false); - }, - [searchValue], - ); - - const debouncedSearch = useMemo( - () => debounce((value: string) => search(value), 500), - [search], - ); - const handleDocumentClick = (e: any) => { if ( searchedResponseRef.current && @@ -73,17 +66,17 @@ export default function SearchBar() { >
setIsFocused(true)} value={searchValue} onChange={(e) => { setSearchValue(e.target.value); if (!e.target.value) return setSearchResponse(undefined); - debouncedSearch(e.target.value); + // debouncedSearch(e.target.value); + debounce(e.target.value); }} placeholder="Search documents..." /> @@ -94,17 +87,15 @@ export default function SearchBar() { setSearchValue("")} - className={`${ - !searchValue ? "hidden" : "block" - } absolute text-slate-500 right-0 top-1/2 transform -translate-y-1/2 mr-2 cursor-pointer`} + className={`${!searchValue ? "hidden" : "block" + } absolute text-slate-500 right-0 top-1/2 transform -translate-y-1/2 mr-2 cursor-pointer`} />
{isSearching ? (
diff --git a/app/layout.tsx b/app/layout.tsx index 6f9842b..b61fee3 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -12,7 +12,7 @@ import "./globals.css"; const inter = Inter({ subsets: ["latin"] }); export const metadata: Metadata = { - title: "Docx", + title: "DocX", description: "An open-source alternative to Google Docs, that lets you write and customize your docs collaboratively with others", }; diff --git a/app/writer/[id]/actions.ts b/app/writer/[id]/actions.ts index 017ecd9..9f90f15 100644 --- a/app/writer/[id]/actions.ts +++ b/app/writer/[id]/actions.ts @@ -104,6 +104,7 @@ export const UpdateThumbnail = async (id: any, thumbnail: string) => { error: "User is not logged in", }; + console.log(process.env.BACKEND_SERVER_URL) const response = await fetch(`${process.env.BACKEND_SERVER_URL}/push-to-quque`, { method: "POST", headers: { diff --git a/app/writer/[id]/components/options/format/ColorHighlight.tsx b/app/writer/[id]/components/options/format/ColorHighlight.tsx index 5ee2d57..3f52a89 100644 --- a/app/writer/[id]/components/options/format/ColorHighlight.tsx +++ b/app/writer/[id]/components/options/format/ColorHighlight.tsx @@ -13,6 +13,11 @@ import { import { Input } from "@/components/ui/input"; export default function ColorHighlight({ editor }: { editor: Editor | null }) { + const colorPopoverRef = useRef(null); + const bgPopoverRef = useRef(null); + + const [isColorPopoverOpen, setIsColorPopoverOpen] = useState(false); + const [isBgPopoverOpen, setIsBgPopoverOpen] = useState(false); const [fontColor, setFontColor] = useState("#000000"); const [highlightColor, setHighlightColor] = useState("#fdfb7a"); @@ -27,6 +32,25 @@ export default function ColorHighlight({ editor }: { editor: Editor | null }) { editor.chain().focus().toggleHighlight({ color: hex }).run(); }; + const handleDocumentClick = (e: any) => { + if ( + (colorPopoverRef.current && + !colorPopoverRef.current.contains(e.target)) + || + (bgPopoverRef.current && + !bgPopoverRef.current.contains(e.target)) + ) { + setIsColorPopoverOpen(false); + setIsBgPopoverOpen(false); + } + }; + useEffect(() => { + document.addEventListener("mousedown", handleDocumentClick); + return () => { + document.removeEventListener("mousedown", handleDocumentClick); + }; + }, []); + return (
editor?.chain().focus().setColor(fontColor).run()} className="hover:bg-slate-100 p-2 rounded border-r" /> - - + + setIsColorPopoverOpen(!isColorPopoverOpen)}> - setFontColor(e.target.value)} - className="mb-4" - /> - +
+ setFontColor(e.target.value)} + className="mb-4" + /> + +
- - + + setIsBgPopoverOpen(!isBgPopoverOpen)}> - setHighlightColor(e.target.value)} - className="mb-4" - /> - +
+ setHighlightColor(e.target.value)} + className="mb-4" + /> + +
diff --git a/app/writer/[id]/page.tsx b/app/writer/[id]/page.tsx index a192538..cf1c255 100644 --- a/app/writer/[id]/page.tsx +++ b/app/writer/[id]/page.tsx @@ -1,10 +1,9 @@ "use client"; -import { useState, useMemo, useCallback, useEffect, useRef } from "react"; +import { useState, useCallback, useEffect, useRef } from "react"; import { useParams } from "next/navigation"; import { EditorContent } from "@tiptap/react"; import { useEditor } from "@tiptap/react"; -import { debounce } from "lodash"; import { toast } from "sonner"; import html2canvas from "html2canvas"; import Collaboration from "@tiptap/extension-collaboration"; @@ -13,6 +12,7 @@ import CollaborationCursor from "@tiptap/extension-collaboration-cursor"; import { ScrollArea } from "@/components/ui/scroll-area"; import { getRandomColor } from "@/helpers/getRandomColor"; import type { Document } from "@prisma/client"; +import useDebounce from "@/lib/customHooks/useDebounce"; import { ydoc, provider, extensions, props } from "./editor/editorConfig"; import { FormatOptions, InsertOptions } from "./components/options"; @@ -24,14 +24,22 @@ import Loading from "./components/EditorLoading"; export default function Dashboard() { const params = useParams(); + const editorRef = useRef(null); + + const [isFirstLoad, setIsFirstLoad] = useState(true); + const [name, setName] = useState(""); const [option, setOption] = useState(0); const [isSaving, setIsSaving] = useState(false); const [docData, setDocData] = useState(undefined); const [status, setStatus] = useState("connecting"); - console.log(status); + // console.log(status); - const editorRef = useRef(null); + useEffect(() => { + setName(localStorage.getItem("name") || ""); + setIsFirstLoad(false); + }, []) + // Doc data fetching useEffect(() => { (async () => { const response = await GetDocDetails(params.id); @@ -61,7 +69,7 @@ export default function Dashboard() { } }, [docData?.id, params.id]); - const saveDoc = useCallback(async (editor: any) => { + const debounce = useDebounce(async (editor: any) => { setIsSaving(true); const response = await UpdateDocData( @@ -74,12 +82,7 @@ export default function Dashboard() { } setIsSaving(false); toast.error(response.error); - }, [params.id, createDocThumbnail]); - - const debouncedSaveDoc = useMemo( - () => debounce((editor: any) => saveDoc(editor), 1000), - [saveDoc], - ); + }, 1000); const editor = useEditor({ onCreate: ({ editor: currentEditor }) => { @@ -97,7 +100,7 @@ export default function Dashboard() { CollaborationCursor.configure({ provider, user: { - name: localStorage.getItem("name"), + name, color: getRandomColor(), }, }), @@ -105,7 +108,9 @@ export default function Dashboard() { editorProps: props, content: "", onUpdate({ editor }) { - debouncedSaveDoc(editor); + if (!isFirstLoad) { + debounce(editor); + } }, }); @@ -128,10 +133,10 @@ export default function Dashboard() { editor .chain() .focus() - .updateUser({ name: localStorage.getItem("name") }) + .updateUser({ name }) .run(); } - }, [editor]); + }, [editor, name]); // Set content of the doc useEffect(() => { diff --git a/components/AvatarList.tsx b/components/AvatarList.tsx index 81dc676..8ddef72 100644 --- a/components/AvatarList.tsx +++ b/components/AvatarList.tsx @@ -1,4 +1,6 @@ -import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; +import React from "react"; + +import { Avatar, AvatarImage } from "@/components/ui/avatar"; import type { User } from "@prisma/client"; import getInitials from "@/helpers/getInitials"; @@ -10,17 +12,20 @@ type AvatarListPropType = { export default function AvatarList({ users }: AvatarListPropType) { return ( -
+
{users.map((e, index) => { return ( - +
+
+

{getInitials(e.user.name ?? "X")}

+
{e.user.picture ? ( - - ) : ( - {getInitials(e.user.name)} - )} - - ); + + + + ) : <>} +
+ ) })}
); diff --git a/lib/customHooks/useDebounce.tsx b/lib/customHooks/useDebounce.tsx new file mode 100644 index 0000000..bdc9c81 --- /dev/null +++ b/lib/customHooks/useDebounce.tsx @@ -0,0 +1,20 @@ +import { debounce } from "lodash"; +import { useEffect, useMemo, useRef } from "react"; + +export default function useDebounce( + callbackFunction: ((value: D) => void) | ((value: D) => Promise), + debounceDelay: number, +) { + const callbackRef = useRef(callbackFunction); + + useEffect(() => { + callbackRef.current = callbackFunction; + }, [callbackFunction ]); + + const debounceFunc = useMemo( + () => debounce((value: D) => callbackRef.current(value), debounceDelay), + [debounceDelay] + ); + + return debounceFunc; +}