diff --git a/app/(auth)/layout.tsx b/app/(auth)/layout.tsx index e9d8604..4bea0bc 100644 --- a/app/(auth)/layout.tsx +++ b/app/(auth)/layout.tsx @@ -1,8 +1,10 @@ import Image from "next/image"; +import SignInImage from '@/public/signin.webp' + export default function AuthLayout({ children }: { children: React.ReactNode }) { return ( -
+
{children} @@ -10,11 +12,9 @@ export default function AuthLayout({ children }: { children: React.ReactNode })
Image
diff --git a/app/(auth)/signin/components/LogInForm.tsx b/app/(auth)/signin/components/LogInForm.tsx index e66c2df..68eac1a 100644 --- a/app/(auth)/signin/components/LogInForm.tsx +++ b/app/(auth)/signin/components/LogInForm.tsx @@ -1,23 +1,27 @@ -import Link from "next/link" +import { useState } from "react" import { useRouter } from "next/navigation" import { useForm } from 'react-hook-form' +import Link from "next/link" import { z } from 'zod' -import { Button } from "@/components/ui/button" import { Input } from "@/components/ui/input" import { Label } from "@/components/ui/label" import { SigninAction } from "../../actions" import { signinSchema } from '../../zodSchema' import { toast } from "sonner" +import LoaderButton from "@/components/LoaderButton" export default function LogInForm() { const router = useRouter() + const [isSubmitting, setIsSubmitting] = useState(false); + const { register, handleSubmit } = useForm>(); const submitForm = async (data: z.infer) => { + setIsSubmitting(true); const parsedData = signinSchema.parse({ email: data.email, password: data.password @@ -26,8 +30,10 @@ export default function LogInForm() { if (response.success) { toast.success("login completed") router.push('/') + setIsSubmitting(false); } else { toast.error(response.error) + setIsSubmitting(false); } }; @@ -56,9 +62,11 @@ export default function LogInForm() { {...register('password', { required: true })} />
- + Sign in ) } diff --git a/app/(auth)/signup/components/SignInForm.tsx b/app/(auth)/signup/components/SignInForm.tsx index f60e8b7..d2f1e5b 100644 --- a/app/(auth)/signup/components/SignInForm.tsx +++ b/app/(auth)/signup/components/SignInForm.tsx @@ -1,23 +1,27 @@ "use client" +import { useState } from "react" import { useRouter } from "next/navigation" import { useForm } from 'react-hook-form' import { toast } from "sonner" -import { Button } from "@/components/ui/button" import { Input } from "@/components/ui/input" import { Label } from "@/components/ui/label" import { z } from 'zod' import { signupSchema } from '../../zodSchema' import { SignupAction } from '../../actions' +import LoaderButton from "@/components/LoaderButton" export default function CredentialsForm() { const router = useRouter(); const { register, handleSubmit } = useForm>(); + const [isSubmitting, setIsSubmitting] = useState(false); + const submitForm = async (data: z.infer) => { + setIsSubmitting(true); const parsedData = signupSchema.parse({ name: data.name, username: data.username, @@ -29,9 +33,11 @@ export default function CredentialsForm() { if (response.success) { toast.success("Signin completed") router.push('/') + setIsSubmitting(false); } else { console.log(response.error); toast.error(response.error); + setIsSubmitting(false); } }; return ( @@ -67,9 +73,11 @@ export default function CredentialsForm() { {...register('password', { required: true })} />
- + Sign up ) } diff --git a/app/components/Card/Card.tsx b/app/components/Card/Card.tsx index 4909f95..8ebf59f 100644 --- a/app/components/Card/Card.tsx +++ b/app/components/Card/Card.tsx @@ -1,6 +1,6 @@ 'use client' -import { useCallback, useMemo, useRef, useState } from "react" +import { useRef, useState } from "react" import { useRouter } from "next/navigation" import { @@ -13,9 +13,9 @@ import { Input } from "@/components/ui/input" import prettifyDate from '@/helpers/prettifyDates' import CardOptions from "./components/Options" -import { debounce } from "lodash" import { RenameDocument } from "./actions" import useClientSession from "@/lib/customHooks/useClientSession" +import { CircleCheck } from "lucide-react" type DocCardPropType = { docId: string; @@ -39,13 +39,6 @@ export default function DocCard({ docId, thumbnail, title, updatedAt, users }: D const [name, setName] = useState(title) - const saveName = useCallback(async () => { - if (!inputRef.current || !session?.id) return; - await RenameDocument(docId, session.id, inputRef.current.value); - }, []) - - const debounceSaveName = useMemo(() => debounce(saveName, 2000), [saveName]) - const getInitials = (name: string) => { let initials = name.split(" "); @@ -62,15 +55,21 @@ export default function DocCard({ docId, thumbnail, title, updatedAt, users }: D > - { - setName(e.target.value) - debounceSaveName(); - }} - /> +
+ setName(e.target.value)} + /> + { + if (!inputRef.current || !session?.id) return; + await RenameDocument(docId, session.id, inputRef.current.value); + }} + className={`${title != name ? "" : "hidden"} size-4 text-slate-500 hover:cursor-pointer absolute right-3 top-1/2 transform -translate-y-1/2`}> + +
{users.map((e, index) => { diff --git a/app/components/Card/components/Input.tsx b/app/components/Card/components/Input.tsx index d6c8afb..041ffc9 100644 --- a/app/components/Card/components/Input.tsx +++ b/app/components/Card/components/Input.tsx @@ -1,23 +1,19 @@ 'use client' import { Input } from "@/components/ui/input"; -import { useRef, useState } from "react" - -type InputPropType = { - title: string; -} - -export default function CardInput({title}:InputPropType) { - const inputRef = useRef(null); - - const [name, setName] = useState(title) +import { CircleCheck } from "lucide-react"; +export default function CardInput({ title, value, ...props }: any) { + console.log(value) return ( - setName(e.target.value)} - /> +
+ + + +
) } diff --git a/app/components/Card/components/Options.tsx b/app/components/Card/components/Options.tsx index d09c645..d486543 100644 --- a/app/components/Card/components/Options.tsx +++ b/app/components/Card/components/Options.tsx @@ -25,6 +25,7 @@ import { Button } from "@/components/ui/button" import { DeleteDocument } from "../actions" import useClientSession from "@/lib/customHooks/useClientSession" +import LoaderButton from "@/components/LoaderButton" type CardOptionsPropType = { docId: string, @@ -43,8 +44,9 @@ export default function CardOptions({ docId, inputRef }: CardOptionsPropType) { { icon: Trash2, color: "#f94848", title: "Delete", onClick: () => deleteDocument() }, ] - const [isOptionsOpen, setIsOptionsOpen] = useState(false) - const [isOpen, setIsOpen] = useState(false) + const [isOptionsOpen, setIsOptionsOpen] = useState(false); + const [isOpen, setIsOpen] = useState(false); + const [isDeleting, setIsDeleting] = useState(false); const renameDocument = () => { if (!inputRef.current) return; @@ -62,6 +64,7 @@ export default function CardOptions({ docId, inputRef }: CardOptionsPropType) { } const confirmDeleteDocument = async () => { + setIsDeleting(true); if (!session.id) return; const response = await DeleteDocument(docId, session.id); @@ -70,6 +73,7 @@ export default function CardOptions({ docId, inputRef }: CardOptionsPropType) { } else { toast.error(response.error) } + setIsDeleting(false); } return ( <> @@ -108,13 +112,14 @@ export default function CardOptions({ docId, inputRef }: CardOptionsPropType) {
- +
diff --git a/app/components/Header/Header.tsx b/app/components/Header/Header.tsx index c976c22..9bfb23f 100644 --- a/app/components/Header/Header.tsx +++ b/app/components/Header/Header.tsx @@ -1,99 +1,18 @@ -"use client" - -import { useState } from "react" -import { useRouter } from "next/navigation" import Image from "next/image" -import { toast } from "sonner" -import { - CloudUpload, - LogOut, - Plus, - Search, -} from "lucide-react" - -import { Button } from "@/components/ui/button" -import { Input } from "@/components/ui/input" -import { CreateNewDocument, LogoutAction } from "./actions" -import logo from "@/public/doc.svg" -import useClientSession from "@/lib/customHooks/useClientSession" +import logo from "@/public/output-onlinepngtools.svg" +import SearchBar from "./components/SearchBar" +import HeaderButtons from "./components/HeaderButtons" export default function Header() { - const router = useRouter(); - - const session = useClientSession(); - - const [isLoading, setIsLoading] = useState(false); - - const createDocument = async () => { - setIsLoading(true); - - if (!session?.id) return; - const response = await CreateNewDocument(session.id); - if (response.success) { - setIsLoading(false); - toast.success("Successfully created new document") - router.push(`/writer/${response.data?.id}`) - } else { - setIsLoading(false); - toast.error(response.error) - } - } - - const logout = async () => { - const response = await LogoutAction(); - console.log(response); - if (response.success) { - toast.success("Successfully logged out") - router.push('/api/auth/signin') - } else { - toast.error(response.error) - } - } return (
logo -

Docx

-
-
- - -
-
- - - + {/*

Docx

*/}
+ +
) } diff --git a/app/components/Header/actions.ts b/app/components/Header/actions.ts index 13063ec..55dd548 100644 --- a/app/components/Header/actions.ts +++ b/app/components/Header/actions.ts @@ -3,7 +3,36 @@ import { cookies } from "next/headers"; import prisma from "@/prisma/prismaClient"; -import { signOut } from "next-auth/react"; +import getServerSession from "@/lib/customHooks/getServerSession"; + +export const SearchDocAction = async (value: string) => { + const session = await getServerSession(); + if (!session.id) return { success: false, error: "Session not found" }; + + try { + const searchResult = await prisma.document.findMany({ + where: { + name: { + contains: value, + mode: 'insensitive' + }, + userId: session.id, + }, + select: { + id: true, + updatedAt: true, + name: true, + users: true + } + }); + + if (searchResult.length > 0) return { success: true, data: searchResult }; + return { success: false, error: "Couldn't find document" }; + } catch (e) { + console.error(e); + return { success: false, error: "Couldn't find document" }; + } +} export const CreateNewDocument = async (userId: string) => { try { diff --git a/app/components/Header/components/HeaderButtons.tsx b/app/components/Header/components/HeaderButtons.tsx new file mode 100644 index 0000000..f1d457e --- /dev/null +++ b/app/components/Header/components/HeaderButtons.tsx @@ -0,0 +1,77 @@ +"use client" + +import { useState } from "react" +import { useRouter } from "next/navigation" +import { toast } from "sonner" +import { + CloudUpload, + LogOut, + PlusIcon, +} from "lucide-react" + +import { Button } from "@/components/ui/button" +import { CreateNewDocument, LogoutAction } from "../actions" +import useClientSession from "@/lib/customHooks/useClientSession" +import LoaderButton from "@/components/LoaderButton" + +export default function HeaderButtons() { + const router = useRouter(); + + const session = useClientSession(); + + const [isLoading, setIsLoading] = useState(false); + + const createDocument = async () => { + setIsLoading(true); + + if (!session?.id) return; + const response = await CreateNewDocument(session.id); + if (response.success) { + setIsLoading(false); + toast.success("Successfully created new document") + router.push(`/writer/${response.data?.id}`) + } else { + setIsLoading(false); + toast.error(response.error) + } + } + + const logout = async () => { + const response = await LogoutAction(); + console.log(response); + if (response.success) { + toast.success("Successfully logged out") + router.push('/api/auth/signin') + } else { + toast.error(response.error) + } + } + return ( +
+
+ + } + > +

Create New

+
+
+ +
+ ) +} diff --git a/app/components/Header/components/SearchBar.tsx b/app/components/Header/components/SearchBar.tsx new file mode 100644 index 0000000..e473d96 --- /dev/null +++ b/app/components/Header/components/SearchBar.tsx @@ -0,0 +1,128 @@ +"use client" + +import { useCallback, useMemo, useState } from "react" +import Image from "next/image"; +import { Search, X } from "lucide-react" +import { debounce } from 'lodash' + +import { Input } from "@/components/ui/input" +import doc from '@/public/output-onlinepngtools.svg' + +import { SearchDocAction } from "../actions"; +import prettifyDate from "@/helpers/prettifyDates"; +import { useRouter } from "next/navigation"; + +type SearchResultType = { + id: string, + name: string, + updatedAt: Date, + users: { + userId: string, + documentId: string, + assignedAt: Date + }[] +} +type SearchResponse = { + success: boolean, + data?: SearchResultType[], + error?: string +} +export default function SearchBar() { + const router = useRouter(); + + const [searchResponse, setSearchResponse] = useState(undefined); + const [searchValue, setSearchValue] = useState(""); + 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] + ); + + return ( + + ) +} diff --git a/app/globals.css b/app/globals.css index a2077be..980b125 100644 --- a/app/globals.css +++ b/app/globals.css @@ -96,3 +96,4 @@ .divider:not(:empty)::after { margin-left: 0.25em; } + diff --git a/app/writer/[id]/actions.ts b/app/writer/[id]/actions.ts index 4131ca8..4203585 100644 --- a/app/writer/[id]/actions.ts +++ b/app/writer/[id]/actions.ts @@ -1,6 +1,5 @@ "use server" -import prettifyDate from "@/helpers/prettifyDates" import prisma from "@/prisma/prismaClient" import { revalidatePath } from "next/cache" diff --git a/app/writer/[id]/components/EditorLoading.tsx b/app/writer/[id]/components/EditorLoading.tsx index bce0467..21cbc4f 100644 --- a/app/writer/[id]/components/EditorLoading.tsx +++ b/app/writer/[id]/components/EditorLoading.tsx @@ -1,6 +1,6 @@ export default function Loading() { return ( -
+
diff --git a/app/writer/[id]/components/options/format/index.tsx b/app/writer/[id]/components/options/format/index.tsx index d1cd786..bb7ecb1 100644 --- a/app/writer/[id]/components/options/format/index.tsx +++ b/app/writer/[id]/components/options/format/index.tsx @@ -53,7 +53,7 @@ export default function FormatOptions({ editor }: { editor: Editor | null }) { const matcher = val.split(" ") if (matcher[0] === "normal") return editor?.commands.setParagraph(); // @ts-ignore - return editor?.chain().focus().setHeading({ level: Number(matcher[1]) }).run(); + return editor?.commands.setHeading({ level: Number(matcher[1]) }); } const setDefaultFontFamily = () => { @@ -69,7 +69,7 @@ export default function FormatOptions({ editor }: { editor: Editor | null }) { return (
-
+
Style @@ -94,11 +94,11 @@ export default function FormatOptions({ editor }: { editor: Editor | null }) {
-
+
Font -
+
-
+
{formattingBtns.map(({ func, name, Icon }, i) => { return ( diff --git a/app/writer/[id]/editor/editorConfig.ts b/app/writer/[id]/editor/editorConfig.ts index 9c195af..3fa182c 100644 --- a/app/writer/[id]/editor/editorConfig.ts +++ b/app/writer/[id]/editor/editorConfig.ts @@ -26,6 +26,6 @@ export const extensions = [ export const props = { attributes: { - class: cn("[&_ol]:list-decimal [&_ul]:list-disc w-[818px] h-[1056px] mx-auto bg-white rounded-md border p-8 shadow-none focus-visible:outline-none") + class: cn("prose [&_ol]:list-decimal [&_ul]:list-disc w-[816.3px] max-w-[816.3px] h-[1056.36px] mx-auto bg-white rounded-md border p-24 my-2 shadow-none focus-visible:outline-none") } } diff --git a/app/writer/[id]/page.tsx b/app/writer/[id]/page.tsx index 6dbc796..bd849d2 100644 --- a/app/writer/[id]/page.tsx +++ b/app/writer/[id]/page.tsx @@ -1,6 +1,12 @@ "use client" -import { useState, useMemo, useCallback, useEffect } from 'react' +import { + useState, + useMemo, + useCallback, + useEffect, + useRef +} from 'react' import { useParams } from 'next/navigation' import { useQuery } from '@tanstack/react-query' import { EditorContent } from '@tiptap/react' @@ -28,6 +34,8 @@ export default function Dashboard() { const [isSaving, setIsSaving] = useState(false); const [docData, setDocData] = useState(undefined); + const editorRef = useRef(null); + const { data } = useQuery({ queryKey: ["doc-details", params.id], queryFn: async () => { @@ -41,8 +49,8 @@ export default function Dashboard() { return null; } }, - retry: 5, - retryDelay: 100, + // retry: 5, + // retryDelay: 100, }) const createDocThumbnail = async () => { @@ -111,15 +119,15 @@ export default function Dashboard() { const [option, setOption] = useState(0) return ( -
+
-
+
{Options[option]} - + {!data ? : - + }
diff --git a/components/LoaderButton.tsx b/components/LoaderButton.tsx new file mode 100644 index 0000000..bbe2b09 --- /dev/null +++ b/components/LoaderButton.tsx @@ -0,0 +1,45 @@ +import { Plus } from "lucide-react"; + +import { Button, ButtonProps } from "./ui/button"; + +type LoaderButtonType = { + onClickFunc?: (() => void) | (() => Promise), + isLoading: boolean, + className: string, + children: React.ReactNode, + icon?: React.ReactNode, +} & ButtonProps; + +export default function LoaderButton({ + title, + onClickFunc, + isLoading, + className, + icon, + children, + ...props }: LoaderButtonType) { + return ( + + ) +} diff --git a/components/ui/input.tsx b/components/ui/input.tsx index 677d05f..264d6a4 100644 --- a/components/ui/input.tsx +++ b/components/ui/input.tsx @@ -11,7 +11,7 @@ const Input = React.forwardRef( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/output-onlinepngtools.svg b/public/output-onlinepngtools.svg new file mode 100644 index 0000000..b2e7e14 --- /dev/null +++ b/public/output-onlinepngtools.svg @@ -0,0 +1,130 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/signin.webp b/public/signin.webp new file mode 100644 index 0000000..ec15617 Binary files /dev/null and b/public/signin.webp differ diff --git a/public/z19dz5c84b2bb2b1e4c418b64605e596cb9bd.png b/public/z19dz5c84b2bb2b1e4c418b64605e596cb9bd.png deleted file mode 100644 index 9995fa9..0000000 Binary files a/public/z19dz5c84b2bb2b1e4c418b64605e596cb9bd.png and /dev/null differ