├── .eslintrc.json
├── .github
└── workflows
│ └── main.yml
├── .gitignore
├── README.md
├── app
├── (auth)
│ ├── DynamicImage.tsx
│ ├── actions.ts
│ ├── forget-password
│ │ └── page.tsx
│ ├── layout.tsx
│ ├── otp
│ │ └── [id]
│ │ │ └── page.tsx
│ ├── reset-password
│ │ └── [id]
│ │ │ └── page.tsx
│ ├── signin
│ │ ├── components
│ │ │ ├── GoogleAuthButton.tsx
│ │ │ └── SignInForm.tsx
│ │ └── page.tsx
│ ├── signup
│ │ ├── components
│ │ │ ├── GoogleAuthButton.tsx
│ │ │ └── SignUpForm.tsx
│ │ └── page.tsx
│ └── zodSchema.ts
├── NextAuthProvider.tsx
├── ReactQueryProvider.tsx
├── api
│ └── auth
│ │ └── [...nextauth]
│ │ └── route.ts
├── components
│ ├── AboutSection.tsx
│ ├── ContactSection.tsx
│ ├── FeaturesSection.tsx
│ ├── Footer.tsx
│ └── HeroSection.tsx
├── document
│ ├── actions.ts
│ ├── components
│ │ ├── Card
│ │ │ ├── Card.tsx
│ │ │ ├── actions.ts
│ │ │ └── components
│ │ │ │ ├── Input.tsx
│ │ │ │ └── Options.tsx
│ │ ├── Header
│ │ │ ├── Header.tsx
│ │ │ ├── actions.ts
│ │ │ └── components
│ │ │ │ ├── HeaderButtons.tsx
│ │ │ │ ├── ProfileBtn.tsx
│ │ │ │ └── SearchBar.tsx
│ │ └── Onboarding.tsx
│ ├── loading.tsx
│ └── page.tsx
├── favicon.ico
├── globals.css
├── layout.tsx
├── page.tsx
└── writer
│ └── [id]
│ ├── actions.ts
│ ├── components
│ ├── BubbleMenuComp
│ │ ├── AskAI.tsx
│ │ ├── GeneratedText.tsx
│ │ ├── generateTextConfig.ts
│ │ └── index.tsx
│ ├── EditorLoading.tsx
│ ├── Header
│ │ └── Header.tsx
│ ├── OptionsResp.tsx
│ ├── Tabs.tsx
│ └── options
│ │ ├── GoBack.tsx
│ │ ├── format
│ │ ├── BulletListBtns.tsx
│ │ ├── ColorHighlight.tsx
│ │ ├── Font.tsx
│ │ ├── FormattingBtns.tsx
│ │ ├── Heading.tsx
│ │ ├── ParagraphBtns.tsx
│ │ ├── functions.ts
│ │ ├── index.tsx
│ │ └── textEditorOptions.ts
│ │ ├── index.ts
│ │ └── insert
│ │ └── index.tsx
│ ├── editor
│ ├── editorConfig.ts
│ └── index.ts
│ └── page.tsx
├── components.json
├── components
├── AvatarList.tsx
├── GridBg.tsx
├── HeaderBtn.tsx
├── HeroImage.tsx
├── LoaderButton.tsx
├── LoopWords.tsx
├── Navbar.tsx
├── ScrollCard.tsx
├── aiAnimation
│ ├── components
│ │ ├── BubbleMenu.tsx
│ │ ├── ColorHighlight.tsx
│ │ └── FormattingBtns.tsx
│ └── index.tsx
└── ui
│ ├── avatar.tsx
│ ├── badge.tsx
│ ├── button.tsx
│ ├── card.tsx
│ ├── dialog.tsx
│ ├── drawer.tsx
│ ├── dropdown.tsx
│ ├── input.tsx
│ ├── label.tsx
│ ├── popover.tsx
│ ├── scroll-area.tsx
│ ├── select.tsx
│ ├── sonner.tsx
│ ├── tabs.tsx
│ ├── textarea.tsx
│ └── tooltip.tsx
├── docker-compose.yaml
├── eslint.js
├── helpers
├── generateJWT.ts
├── getInitials.ts
├── getRandomColor.ts
└── prettifyDates.ts
├── lib
├── auth.ts
├── customHooks
│ ├── ReturnType.ts
│ ├── action.ts
│ ├── getServerSession.ts
│ ├── useClientSession.tsx
│ ├── useDebounce.tsx
│ └── useMotionTimeline.tsx
├── guestServices.ts
├── mail
│ ├── EmailVerificationMailTemplate.tsx
│ ├── resetPasswordMailTemplate.tsx
│ └── sendMail.ts
└── utils.ts
├── middleware.ts
├── next-env.d.ts
├── next.config.mjs
├── package-lock.json
├── package.json
├── postcss.config.mjs
├── prisma
├── migrations
│ ├── 20240709131843_init
│ │ └── migration.sql
│ ├── 20240715060410_15_07_2024
│ │ └── migration.sql
│ ├── 20240715112437_
│ │ └── migration.sql
│ ├── 20240715114926_schema_change
│ │ └── migration.sql
│ ├── 20240715134816_
│ │ └── migration.sql
│ ├── 20240716082106_cascade_delete
│ │ └── migration.sql
│ ├── 20240717054532_thumbnail
│ │ └── migration.sql
│ ├── 20240724135206_init2
│ │ └── migration.sql
│ ├── 20240804151601_new_changes
│ │ └── migration.sql
│ ├── 20240805092134_password_null
│ │ └── migration.sql
│ ├── 20240805092953_picture_null
│ │ └── migration.sql
│ ├── 20241020044532_otp
│ │ └── migration.sql
│ └── migration_lock.toml
├── prismaClient.ts
└── schema.prisma
├── public
├── 110045644.png
├── Forgot password.webp
├── Hero image mobile view.webp
├── Hero video thumbnail.png
├── Hero_section_image.png
├── Reset password.webp
├── Signup.webp
├── Toolbar.png
├── Verify otp.webp
├── ai-feature.png
├── collab-feature.png
├── createdoc.png
├── favicon.ico
├── github.png
├── google_icon.svg
├── grid_bg.svg
├── logo.png
├── logo.svg
├── mask.svg
├── output-onlinepngtools.svg
├── profilepic_placeholder.png
└── signin.webp
├── tailwind.config.ts
├── tsconfig.json
└── yarn.lock
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "next/core-web-vitals"
3 | }
4 |
--------------------------------------------------------------------------------
/.github/workflows/main.yml:
--------------------------------------------------------------------------------
1 | name: CI/CD Pipeline
2 |
3 | on:
4 | pull_request:
5 | branches:
6 | - "**"
7 | paths-ignore:
8 | - "README.md"
9 |
10 | permissions:
11 | id-token: write
12 | contents: read
13 |
14 | jobs:
15 | Continuous_Integration:
16 | name: Check for Linting and Formatting
17 | runs-on: ubuntu-latest
18 | steps:
19 | - name: Checkout Code
20 | uses: actions/checkout@v3
21 |
22 | - name: Install dependancies
23 | run: npm install
24 |
25 | - name: Lint code
26 | run: npm run lint:check
27 |
28 | - name: Check Formatting
29 | run: npm run format:check
30 |
31 | - name: Run unit tests
32 | run: echo "Running unit tests"
33 |
34 | # build-and-push-ecr-image:
35 | # name: Build and Push to ECR
36 | # needs: integration
37 | # runs-on: ubuntu-latest
38 | # steps:
39 | # - name: Checkout Code
40 | # uses: actions/checkout@v2
41 |
42 | # - name: Configure AWS credentials
43 | # uses: aws-actions/configure-aws-credentials@v1
44 | # with:
45 | # aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
46 | # aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
47 | # aws-region: ${{ secrets.AWS_DEFAULT_REGION }}
48 |
49 | # - name: Login to Amazon ECR
50 | # id: login-ecr
51 | # uses: aws-actions/amazon-ecr-login@v1
52 |
53 | # - name: Build and tag Docker images
54 | # run: |
55 | # # Build Docker containers and tag them.
56 | # docker compose -f D:\Projects\Google docs clone\docker-compose.yaml build
57 |
58 | # - name: Push Docker images to Amazon ECR
59 | # run: |
60 | # # Push the Docker images to Amazon ECR.
61 | # docker compose -f D:\Projects\Google docs clone\docker-compose.yaml push
62 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | node_modules
5 | /.pnp
6 | .pnp.js
7 | .yarn/install-state.gz
8 |
9 | # testing
10 | /coverage
11 |
12 | # next.js
13 | /.next/
14 | /out/
15 |
16 | # production
17 | /build
18 |
19 | # misc
20 | .DS_Store
21 | *.pem
22 |
23 | # debug
24 | npm-debug.log*
25 | yarn-debug.log*
26 | yarn-error.log*
27 |
28 | # local env files
29 | .env*.local
30 |
31 | # vercel
32 | .vercel
33 |
34 | # typescript
35 | *.tsbuildinfo
36 | next-env.d.ts
37 |
38 | .env
39 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app).
2 |
3 | ## Getting Started
4 |
5 | First, run the development server:
6 |
7 | ```bash
8 | npm run dev
9 | # or
10 | yarn dev
11 | # or
12 | pnpm dev
13 | # or
14 | bun dev
15 | ```
16 |
17 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
18 |
19 | You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
20 |
21 | This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font.
22 |
23 | ## Learn More
24 |
25 | To learn more about Next.js, take a look at the following resources:
26 |
27 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
28 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
29 |
30 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!
31 |
32 | ## Deploy on Vercel
33 |
34 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
35 |
36 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.
37 |
--------------------------------------------------------------------------------
/app/(auth)/DynamicImage.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { useCallback, useEffect, useState } from "react";
4 | import { usePathname } from "next/navigation";
5 | import Image from "next/image";
6 |
7 | import SignIn from "@/public/signin.webp";
8 | import SignUp from "@/public/Signup.webp";
9 | import ResetPassword from "@/public/Reset password.webp";
10 | import ForgotPassword from "@/public/Forgot password.webp";
11 | import VerifyOtp from "@/public/Verify otp.webp";
12 |
13 | const images = [
14 | { path: "signin", image: SignIn },
15 | { path: "signup", image: SignUp },
16 | { path: "reset-password", image: ResetPassword },
17 | { path: "forget-password", image: ForgotPassword },
18 | { path: "otp", image: VerifyOtp },
19 | ];
20 | export default function DynamicImage() {
21 | const url = usePathname();
22 |
23 | const getImageFromRoute = useCallback(() => {
24 | return images.find((e) => e.path === url.split("/")[1])?.image || "";
25 | }, [url]);
26 |
27 | const [currentImage, setCurrentImage] = useState(getImageFromRoute());
28 |
29 | useEffect(() => {
30 | setCurrentImage(getImageFromRoute());
31 | }, [getImageFromRoute, url]);
32 |
33 | return (
34 |
39 | );
40 | }
41 |
--------------------------------------------------------------------------------
/app/(auth)/forget-password/page.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { useState } from "react";
4 | import { useRouter } from "next/navigation";
5 | import { toast } from "sonner";
6 | import { KeyRound } from "lucide-react";
7 |
8 | import LoaderButton from "@/components/LoaderButton";
9 | import { Input } from "@/components/ui/input";
10 | import { Label } from "@/components/ui/label";
11 | import { sendResetPasswordMail } from "../actions";
12 |
13 | export default function ForgetPassoword() {
14 | const router = useRouter();
15 |
16 | const [inputEmail, setInputEmail] = useState("");
17 | const [isSubmitting, setIsSubmitting] = useState(false);
18 |
19 | const sendMail = async () => {
20 | setIsSubmitting(true);
21 | try {
22 | const response = await sendResetPasswordMail(inputEmail);
23 | if (response.success) {
24 | toast.success(response.data);
25 | router.push(`/signin`);
26 | setIsSubmitting(false);
27 | } else {
28 | console.log(response.error);
29 | toast.error(response.error);
30 | setIsSubmitting(false);
31 | }
32 | } catch (e) {
33 | console.log(e);
34 | setIsSubmitting(false);
35 | }
36 | };
37 | return (
38 | <>
39 |
40 |
41 |
42 | Forgot your
43 | Password ?
44 |
45 |
46 | Enter the email address , and we'll send you a link to reset your
47 | password.
48 |
49 |
50 |
51 | Email
52 | setInputEmail(e.target.value)}
56 | />
57 |
62 | Send email
63 |
64 |
65 | >
66 | );
67 | }
68 |
--------------------------------------------------------------------------------
/app/(auth)/layout.tsx:
--------------------------------------------------------------------------------
1 | import DynamicImage from "./DynamicImage";
2 |
3 | export default function AuthLayout({
4 | children,
5 | }: {
6 | children: React.ReactNode;
7 | }) {
8 | return (
9 |
17 | );
18 | }
19 |
--------------------------------------------------------------------------------
/app/(auth)/otp/[id]/page.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { useEffect, useState } from "react";
4 | import { useParams, useRouter } from "next/navigation";
5 | import { toast } from "sonner";
6 | import { MailCheck } from "lucide-react";
7 |
8 | import LoaderButton from "@/components/LoaderButton";
9 | import { Input } from "@/components/ui/input";
10 | import { resendVerifyCode, verifyEmail } from "../../actions";
11 |
12 | export default function OTP() {
13 | const params = useParams();
14 | const router = useRouter();
15 |
16 | const userEmail = params.id.toString().replace("%40", "@");
17 |
18 | const [inputValue, setInputValue] = useState("");
19 | const [isSubmitting, setIsSubmitting] = useState(false);
20 | const [isResending, setIsResending] = useState(false);
21 | const [timer, setTimer] = useState(5);
22 |
23 | useEffect(() => {
24 | const interval = setInterval(() => {
25 | setTimer((prev) => {
26 | if (prev <= 1) {
27 | // clearInterval(interval);
28 | return 0;
29 | }
30 | return prev - 1;
31 | });
32 | }, 1000);
33 |
34 | return () => {
35 | clearInterval(interval);
36 | };
37 | }, []);
38 |
39 | const verify = async () => {
40 | setIsSubmitting(true);
41 | try {
42 | const response = await verifyEmail(inputValue, userEmail);
43 | if (response.success) {
44 | toast.success(response.data);
45 | router.push(`/document`);
46 | setIsSubmitting(false);
47 | } else {
48 | console.log(response.error);
49 | toast.error(response.error);
50 | setIsSubmitting(false);
51 | }
52 | } catch (e) {
53 | console.log(e);
54 | setIsSubmitting(false);
55 | }
56 | };
57 |
58 | const resendCode = async () => {
59 | setIsResending(true);
60 | try {
61 | const response = await resendVerifyCode(userEmail);
62 | if (response.success) {
63 | toast.success(response.data);
64 | setTimer(121);
65 | setIsResending(false);
66 | } else {
67 | console.log(response.error);
68 | toast.error(response.error);
69 | setIsResending(false);
70 | }
71 | } catch (e) {
72 | console.log(e);
73 | setIsResending(false);
74 | }
75 | };
76 | return (
77 | <>
78 |
79 |
80 |
81 | Please check your
82 | mail
83 |
84 |
85 | We've sent a code to {userEmail}
86 |
87 |
88 |
89 |
setInputValue(e.target.value)}
93 | />
94 |
99 | Verify
100 |
101 | {timer === 0 ? (
102 |
107 | {isResending ? (
108 |
120 |
121 |
122 | ) : (
123 | <>>
124 | )}
125 | Resend code
126 |
127 | ) : (
128 |
129 | Resend code in: {Math.floor(timer / 60)}:
130 | {timer - Math.floor(timer / 60) * 60 < 10
131 | ? "0" + String(timer - Math.floor(timer / 60) * 60)
132 | : timer - Math.floor(timer / 60) * 60}
133 |
134 | )}
135 |
136 | >
137 | );
138 | }
139 |
--------------------------------------------------------------------------------
/app/(auth)/reset-password/[id]/page.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { useState } from "react";
4 | import { useParams, useRouter } from "next/navigation";
5 | import { toast } from "sonner";
6 | import { KeyRound } from "lucide-react";
7 |
8 | import LoaderButton from "@/components/LoaderButton";
9 | import { Input } from "@/components/ui/input";
10 | import { Label } from "@/components/ui/label";
11 | import { resetPassword } from "../../actions";
12 | import { passwordValidation } from "../../zodSchema";
13 |
14 | export default function ResetPassword() {
15 | const params = useParams();
16 | const router = useRouter();
17 |
18 | const [newPassword, setNewPassword] = useState("");
19 | const [confirmNewPassword, setConfirmNewPassword] = useState("");
20 | const [isSubmitting, setIsSubmitting] = useState(false);
21 | const [isRegexError, setIsRegexError] = useState(false);
22 |
23 | const changePassword = async () => {
24 | setIsSubmitting(true);
25 | try {
26 | if (passwordValidation.test(newPassword)) {
27 | if (newPassword !== confirmNewPassword) {
28 | setIsSubmitting(false);
29 | return toast.error("New password and confirm password do not match.");
30 | }
31 |
32 | const response = await resetPassword(params.id, newPassword);
33 | if (response.success) {
34 | toast.success(response.data);
35 | router.push(`/signin`);
36 | setIsSubmitting(false);
37 | } else {
38 | console.log(response.error);
39 | toast.error(response.error);
40 | setIsSubmitting(false);
41 | }
42 | } else {
43 | setIsRegexError(true);
44 | setIsSubmitting(false);
45 | }
46 | } catch (e) {
47 | console.log(e);
48 | setIsSubmitting(false);
49 | }
50 | };
51 | return (
52 | <>
53 |
54 |
55 |
56 | Change your
57 | Password
58 |
59 |
60 |
97 | >
98 | );
99 | }
100 |
--------------------------------------------------------------------------------
/app/(auth)/signin/components/GoogleAuthButton.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import Image from "next/image";
4 | import { signIn } from "next-auth/react";
5 |
6 | import Google from "@/public/google_icon.svg";
7 | import { Button } from "@/components/ui/button";
8 |
9 | export default function GoogleAuthButton() {
10 | return (
11 | signIn("google")}
15 | >
16 |
17 | Continue with Google
18 |
19 | );
20 | }
21 |
--------------------------------------------------------------------------------
/app/(auth)/signin/components/SignInForm.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { useState } from "react";
4 | import { useRouter } from "next/navigation";
5 | import { useForm } from "react-hook-form";
6 | import { toast } from "sonner";
7 | import { Eye, EyeOff } from "lucide-react";
8 | import { z } from "zod";
9 |
10 | import { Input } from "@/components/ui/input";
11 | import { Label } from "@/components/ui/label";
12 | import LoaderButton from "@/components/LoaderButton";
13 |
14 | import { SigninAction } from "../../actions";
15 | import { signinSchema } from "../../zodSchema";
16 |
17 | export default function CredentialsForm() {
18 | const router = useRouter();
19 |
20 | const { register, handleSubmit } = useForm>();
21 |
22 | const [isSubmitting, setIsSubmitting] = useState(false);
23 | const [isPasswordVisible, setIsPasswordVisible] = useState(false);
24 |
25 | const submitForm = async (data: z.infer) => {
26 | setIsSubmitting(true);
27 | const parsedData = signinSchema.parse({
28 | email: data.email,
29 | password: data.password,
30 | });
31 | const response = await SigninAction(parsedData);
32 | if (response.success) {
33 | toast.success("login completed");
34 | router.push("/document");
35 | setIsSubmitting(false);
36 | } else {
37 | toast.error(response.error);
38 | setIsSubmitting(false);
39 | }
40 | };
41 |
42 | return (
43 |
91 | );
92 | }
93 |
--------------------------------------------------------------------------------
/app/(auth)/signin/page.tsx:
--------------------------------------------------------------------------------
1 | import Link from "next/link";
2 |
3 | import SignInForm from "./components/SignInForm";
4 | import GoogleAuthButton from "./components/GoogleAuthButton";
5 |
6 | export default function Login() {
7 | return (
8 | <>
9 |
10 |
11 | Sign
12 | in
13 |
14 |
15 | Enter your email below to login to your account
16 |
17 |
18 |
19 |
20 |
OR
21 |
22 |
23 | Don't have an account?{" "}
24 |
25 | Sign up
26 |
27 |
28 |
29 | >
30 | );
31 | }
32 |
--------------------------------------------------------------------------------
/app/(auth)/signup/components/GoogleAuthButton.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import Image from "next/image";
4 | import { signIn } from "next-auth/react";
5 |
6 | import Google from "@/public/google_icon.svg";
7 | import { Button } from "@/components/ui/button";
8 |
9 | export default function GoogleAuthButton() {
10 | return (
11 | signIn("google", { redirect: true })}
15 | >
16 |
17 | Continue with Google
18 |
19 | );
20 | }
21 |
--------------------------------------------------------------------------------
/app/(auth)/signup/components/SignUpForm.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { useState } from "react";
4 | import { useRouter } from "next/navigation";
5 | import { useForm } from "react-hook-form";
6 | import { toast } from "sonner";
7 | import { z, ZodError } from "zod";
8 | import { Eye, EyeOff } from "lucide-react";
9 |
10 | import { Input } from "@/components/ui/input";
11 | import { Label } from "@/components/ui/label";
12 | import LoaderButton from "@/components/LoaderButton";
13 |
14 | import { SignupAction } from "../../actions";
15 | import { signupSchema } from "../../zodSchema";
16 |
17 | export default function CredentialsForm() {
18 | const router = useRouter();
19 |
20 | const { register, handleSubmit } = useForm>();
21 |
22 | const [isSubmitting, setIsSubmitting] = useState(false);
23 | const [isPasswordVisible, setIsPasswordVisible] = useState(false);
24 | const [isRegexError, setIsRegexError] = useState(false);
25 |
26 | const submitForm = async (data: z.infer) => {
27 | setIsSubmitting(true);
28 | try {
29 | const parsedData = signupSchema.safeParse({
30 | name: data.name,
31 | username: data.username,
32 | email: data.email,
33 | password: data.password,
34 | });
35 | if (!parsedData.success) {
36 | if (parsedData.error.issues[0].code === "invalid_string")
37 | setIsRegexError(true);
38 | setIsSubmitting(false);
39 | }
40 |
41 | if (!parsedData.data) return;
42 |
43 | const response = await SignupAction(parsedData.data);
44 | if (response.success) {
45 | toast.success("User registerd successfully. Please verify your email.");
46 | router.push(`/otp/${data.email}`);
47 | setIsSubmitting(false);
48 | } else {
49 | console.log(response.error);
50 | console.log("here");
51 | toast.error(response.error);
52 | setIsSubmitting(false);
53 | }
54 | } catch (e: ZodError | any) {
55 | console.log(e);
56 | setIsSubmitting(false);
57 | }
58 | };
59 | return (
60 |
123 | );
124 | }
125 |
--------------------------------------------------------------------------------
/app/(auth)/signup/page.tsx:
--------------------------------------------------------------------------------
1 | import Link from "next/link";
2 |
3 | import SignUpForm from "./components/SignUpForm";
4 | import GoogleAuthButton from "./components/GoogleAuthButton";
5 |
6 | export default function Signup() {
7 | return (
8 | <>
9 |
10 |
11 | Sign
12 | up
13 |
14 |
15 | Enter your credentials below to login to your account
16 |
17 |
18 |
19 |
20 |
OR
21 |
22 |
23 | Already have an account?{" "}
24 |
25 | Sign in
26 |
27 |
28 |
29 | >
30 | );
31 | }
32 |
--------------------------------------------------------------------------------
/app/(auth)/zodSchema.ts:
--------------------------------------------------------------------------------
1 | import { z } from "zod";
2 |
3 | // Minimum 8 characters, at least one uppercase letter, one lowercase letter, one number and one special character
4 | export const passwordValidation = new RegExp(
5 | /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,}$/,
6 | );
7 |
8 | export const signupSchema = z.object({
9 | name: z.string().min(1).max(50),
10 | username: z.string().min(1).max(20),
11 | email: z.string().email(),
12 | password: z.string().min(1).regex(passwordValidation),
13 | });
14 |
15 | export const signinSchema = z.object({
16 | email: z.string().email(),
17 | password: z.string(),
18 | });
19 |
--------------------------------------------------------------------------------
/app/NextAuthProvider.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { ReactNode } from "react";
4 | import { SessionProvider } from "next-auth/react";
5 |
6 | export const NextAuthProvider = ({ children }: { children: ReactNode }) => {
7 | return {children} ;
8 | };
9 |
--------------------------------------------------------------------------------
/app/ReactQueryProvider.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import React from "react";
4 | import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
5 |
6 | const queryClient = new QueryClient();
7 |
8 | export default function ReactQueryProvider({
9 | children,
10 | }: {
11 | children: React.ReactNode;
12 | }) {
13 | return (
14 | {children}
15 | );
16 | }
17 |
--------------------------------------------------------------------------------
/app/api/auth/[...nextauth]/route.ts:
--------------------------------------------------------------------------------
1 | import { authOptions } from "@/lib/auth";
2 | import NextAuth from "next-auth";
3 |
4 | const handler = NextAuth(authOptions);
5 |
6 | export { handler as GET, handler as POST };
7 |
--------------------------------------------------------------------------------
/app/components/AboutSection.tsx:
--------------------------------------------------------------------------------
1 | export default function AboutSection() {
2 | return (
3 |
7 |
8 |
9 |
10 | About
11 | DocX
12 |
13 |
14 | DocX is an open-source Google Docs alternative, created by a team of
15 | passionate developers who believe in the power of collaborative
16 | editing. Our mission is to provide a user-friendly, feature-rich
17 | platform that empowers teams to work together seamlessly.
18 |
19 |
20 |
21 |
22 | );
23 | }
24 |
--------------------------------------------------------------------------------
/app/components/ContactSection.tsx:
--------------------------------------------------------------------------------
1 | import { Button } from "@/components/ui/button";
2 | import { Input } from "@/components/ui/input";
3 |
4 | export default function ContactSection() {
5 | return (
6 |
36 | );
37 | }
38 |
--------------------------------------------------------------------------------
/app/components/FeaturesSection.tsx:
--------------------------------------------------------------------------------
1 | import { Cloud, Users, Wand } from "lucide-react";
2 | import * as motion from "framer-motion/client";
3 | import AiAnimation from "@/components/aiAnimation";
4 | import AiFeature from "@/public/ai-feature.png"
5 | import CollabFeature from "@/public/collab-feature.png"
6 | import Image, { StaticImageData } from "next/image";
7 |
8 | export default function FeaturesSection() {
9 | const features = [
10 | {
11 | title: "AI powered",
12 | description:
13 | "Leverage advanced AI capabilities, offering intelligent assistance and seamless automation without additional costs",
14 | animation: ,
15 | image: AiFeature
16 | },
17 | {
18 | title: "Real-Time Collaboration ",
19 | description:
20 | "Multiple users can edit the same document simultaneously, with changes syncing in real-time.",
21 | animation: ,
22 | image: CollabFeature
23 | },
24 | // {
25 | // title: "Highly scalable",
26 | // description:
27 | // "Adapts effortlessly to growth, maintaining performance as needs expand.",
28 | // animation: ,
29 | // image: AiFeature
30 | // },
31 | ];
32 |
33 | return (
34 |
38 |
39 |
40 |
48 | Key
49 | Features
50 |
51 |
52 |
53 | {features.map((feature, index) => (
54 |
55 | ))}
56 |
57 |
58 |
59 | );
60 | }
61 |
62 | function Feature({
63 | title,
64 | description,
65 | animation,
66 | image,
67 | index,
68 | }: {
69 | title: string;
70 | description: string;
71 | animation: React.ReactNode;
72 | image: StaticImageData,
73 | index: number;
74 | }) {
75 | const featureSize = "h-60 w-96";
76 |
77 | return (
78 |
81 |
92 | {/* {animation} */}
93 |
94 |
95 | {/*
*/}
102 |
103 | {/*
*/}
110 |
111 |
112 |
123 | {title}
124 | {description}
125 |
126 |
127 | );
128 | }
129 |
130 | const FeaturesVariant = {
131 | hidden: {
132 | y: 20,
133 | opacity: 0,
134 | },
135 | visible: {
136 | y: 0,
137 | opacity: 1,
138 | },
139 | };
140 |
141 | const FeatureVariant = {
142 | hiddenLeft: {
143 | x: "-100%",
144 | opacity: 0,
145 | },
146 | hiddenRight: {
147 | x: "100%",
148 | opacity: 0,
149 | },
150 | visible: {
151 | x: "0%",
152 | opacity: 1,
153 | },
154 | };
155 |
--------------------------------------------------------------------------------
/app/components/Footer.tsx:
--------------------------------------------------------------------------------
1 | import Image from "next/image";
2 | import { Montserrat_Alternates as Montserrat } from "next/font/google";
3 | import { Github, Star } from "lucide-react";
4 |
5 | import logo from "@/public/logo.svg";
6 |
7 | const roboto = Montserrat({
8 | weight: "500",
9 | style: "normal",
10 | subsets: ["cyrillic"],
11 | });
12 | export default function Footer() {
13 | return (
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
24 | DocX
25 |
26 |
27 |
28 | Copyright © 2025 DocX. All rights reserved.
29 |
30 |
31 |
32 |
98 |
99 |
100 |
101 |
102 | );
103 | }
104 |
--------------------------------------------------------------------------------
/app/components/HeroSection.tsx:
--------------------------------------------------------------------------------
1 | import Link from "next/link";
2 | import * as motion from "framer-motion/client";
3 |
4 | import GridBg from "@/components/GridBg";
5 | import Navbar from "@/components/Navbar";
6 | import LoopWords from "@/components/LoopWords";
7 | import HeroImage from "@/components/HeroImage";
8 |
9 | export default function HeroSection() {
10 | return (
11 | <>
12 |
13 |
14 |
15 |
23 |
24 |
25 |
26 |
36 |
37 | Unlock the
38 |
39 |
40 |
41 |
42 |
43 | of AI-Driven Editing.
44 |
45 |
56 | DocX is an open-source AI powered alternative to Google Docs
57 | that empowers teams to create, edit, and collaborate seamlessly.
58 |
59 |
60 |
71 |
76 | Try DocX
77 |
78 |
83 | Learn More
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 | >
92 | );
93 | }
94 |
95 | const HeroVariant = {
96 | hidden: {
97 | y: 10,
98 | opacity: 0,
99 | },
100 | visible: {
101 | y: 0,
102 | opacity: 1,
103 | },
104 | };
105 |
--------------------------------------------------------------------------------
/app/document/actions.ts:
--------------------------------------------------------------------------------
1 | "use server";
2 |
3 | import getServerSession from "@/lib/customHooks/getServerSession";
4 | import prisma from "@/prisma/prismaClient";
5 |
6 | export const GetAllDocs = async (userId: string) => {
7 | try {
8 | const session = await getServerSession();
9 | if (!session.id)
10 | return {
11 | success: false,
12 | error: "User is not logged in",
13 | };
14 |
15 | const documents = await prisma.document.findMany({
16 | where: {
17 | users: {
18 | some: {
19 | user: {
20 | id: userId,
21 | },
22 | },
23 | },
24 | },
25 | select: {
26 | id: true,
27 | thumbnail: true,
28 | name: true,
29 | updatedAt: true,
30 | createdBy: true,
31 | users: {
32 | select: {
33 | user: {
34 | select: {
35 | name: true,
36 | picture: true,
37 | },
38 | },
39 | },
40 | },
41 | },
42 | orderBy: { updatedAt: "desc" },
43 | });
44 |
45 | if (!documents)
46 | return {
47 | success: false,
48 | error: "There is some problem fetching documents.",
49 | };
50 |
51 | return { success: true, data: documents };
52 | } catch (e) {
53 | console.log(e);
54 | return { success: false, error: "Internal server error" };
55 | }
56 | };
57 |
--------------------------------------------------------------------------------
/app/document/components/Card/Card.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { useRef, useState } from "react";
4 | import { useRouter } from "next/navigation";
5 |
6 | import { Card, CardContent, CardFooter } from "@/components/ui/card";
7 | import { Input } from "@/components/ui/input";
8 | import type { User } from "@prisma/client";
9 |
10 | import { RenameDocument } from "./actions";
11 | import AvatarList from "@/components/AvatarList";
12 | import CardOptions from "./components/Options";
13 | import prettifyDate from "@/helpers/prettifyDates";
14 | import useClientSession from "@/lib/customHooks/useClientSession";
15 | import useDebounce from "@/lib/customHooks/useDebounce";
16 | import { updateGuestDocument } from "@/lib/guestServices";
17 |
18 | type DocCardPropType = {
19 | docId: string;
20 | thumbnail: string | null;
21 | title: string;
22 | updatedAt: Date;
23 | users: {
24 | user: Pick;
25 | }[];
26 | };
27 | export default function DocCard({
28 | docId,
29 | thumbnail,
30 | title,
31 | updatedAt,
32 | users,
33 | }: DocCardPropType) {
34 | const router = useRouter();
35 |
36 | const debounce = useDebounce(async () => {
37 | if (!inputRef.current) return;
38 | if (session?.id) {
39 | await RenameDocument(docId, inputRef.current.value);
40 | } else {
41 | updateGuestDocument(docId, 'name', inputRef.current.value);
42 | }
43 | }, 1000);
44 |
45 | const session = useClientSession();
46 | localStorage.setItem("name", session.name as string);
47 |
48 | const inputRef = useRef(null);
49 |
50 | const [name, setName] = useState(title);
51 |
52 | return (
53 |
54 | router.push(`/writer/${docId}`)}
58 | >
59 |
60 |
61 | {
66 | setName(e.target.value);
67 | debounce(e.target.value);
68 | }}
69 | />
70 |
71 |
72 |
73 |
74 |
75 | {prettifyDate(String(updatedAt), {
76 | year: "numeric",
77 | month: "short",
78 | day: "2-digit",
79 | })}
80 |
81 |
82 |
83 |
84 |
85 |
86 | );
87 | }
88 |
--------------------------------------------------------------------------------
/app/document/components/Card/actions.ts:
--------------------------------------------------------------------------------
1 | "use server";
2 |
3 | import { revalidatePath } from "next/cache";
4 |
5 | import getServerSession from "@/lib/customHooks/getServerSession";
6 | import prisma from "@/prisma/prismaClient";
7 |
8 | export const DeleteDocument = async (docId: any) => {
9 | try {
10 | const session = await getServerSession();
11 | if (!session.id)
12 | return {
13 | success: false,
14 | error: "User is not logged in",
15 | };
16 |
17 | const doc = await prisma.document.findFirst({
18 | where: {
19 | id: docId,
20 | userId: session.id,
21 | },
22 | });
23 | if (!doc) {
24 | return {
25 | success: false,
26 | error: "Document does not exist",
27 | };
28 | }
29 |
30 | await prisma.document.delete({
31 | where: {
32 | id: docId,
33 | userId: session.id,
34 | },
35 | });
36 | revalidatePath("/");
37 |
38 | return { success: true, data: "Document successfully deleted" };
39 | } catch (e) {
40 | console.log(e);
41 | return { success: false, error: "Internal server error" };
42 | }
43 | };
44 |
45 | export const RenameDocument = async (docId: any, newName: string) => {
46 | try {
47 | const session = await getServerSession();
48 | if (!session.id)
49 | return {
50 | success: false,
51 | error: "User is not logged in",
52 | };
53 |
54 | const doc = await prisma.document.findFirst({
55 | where: {
56 | id: docId,
57 | userId: session.id,
58 | },
59 | });
60 | if (!doc) {
61 | return {
62 | success: false,
63 | error: "Document does not exist",
64 | };
65 | }
66 |
67 | await prisma.document.update({
68 | where: {
69 | id: docId,
70 | userId: session.id,
71 | },
72 | data: { name: newName },
73 | });
74 | revalidatePath("/");
75 |
76 | return { success: true, data: "Document successfully renamed" };
77 | } catch (e) {
78 | console.log(e);
79 | return { success: false, error: "Internal server error" };
80 | }
81 | };
82 |
--------------------------------------------------------------------------------
/app/document/components/Card/components/Input.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { Input } from "@/components/ui/input";
4 | import { CircleCheck } from "lucide-react";
5 |
6 | export default function CardInput({ title, value, ...props }: any) {
7 | console.log(value);
8 | return (
9 |
10 |
11 |
14 |
15 | );
16 | }
17 |
--------------------------------------------------------------------------------
/app/document/components/Header/Header.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import Image from "next/image";
4 | import { Montserrat_Alternates as Montserrat } from "next/font/google";
5 | import * as motion from "framer-motion/client";
6 |
7 | import { SessionReturnType } from "@/lib/customHooks/ReturnType";
8 | import logo from "@/public/logo.svg";
9 |
10 | import SearchBar from "./components/SearchBar";
11 | import HeaderButtons from "./components/HeaderButtons";
12 | import ProfileBtn from "./components/ProfileBtn";
13 | import useClientSession from "@/lib/customHooks/useClientSession";
14 |
15 | const roboto = Montserrat({
16 | weight: "500",
17 | style: "normal",
18 | subsets: ["cyrillic"],
19 | });
20 |
21 | type HeaderPropType = Pick;
22 | export default function Header({ image, name }: HeaderPropType) {
23 | const session = useClientSession();
24 |
25 | return (
26 | <>
27 |
39 |
40 |
41 |
42 |
58 | DocX
59 |
60 |
61 |
62 | {session?.id && }
63 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 | >
78 | );
79 | }
80 |
--------------------------------------------------------------------------------
/app/document/components/Header/actions.ts:
--------------------------------------------------------------------------------
1 | "use server";
2 |
3 | import { cookies } from "next/headers";
4 |
5 | import prisma from "@/prisma/prismaClient";
6 | import getServerSession from "@/lib/customHooks/getServerSession";
7 |
8 | export const SearchDocAction = async (value: string) => {
9 | const session = await getServerSession();
10 | if (!session.id)
11 | return {
12 | success: false,
13 | error: "User is not logged in",
14 | };
15 |
16 | try {
17 | const searchResult = await prisma.document.findMany({
18 | where: {
19 | name: {
20 | contains: value,
21 | mode: "insensitive",
22 | },
23 | users: {
24 | some: {
25 | user: {
26 | id: session?.id,
27 | },
28 | },
29 | },
30 | },
31 | select: {
32 | id: true,
33 | updatedAt: true,
34 | createdBy: {
35 | select: { name: true },
36 | },
37 | name: true,
38 | // users: true
39 | },
40 | });
41 |
42 | if (searchResult.length > 0) return { success: true, data: searchResult };
43 | return { success: false, error: "Couldn't find document" };
44 | } catch (e) {
45 | console.error(e);
46 | return { success: false, error: "Couldn't find document" };
47 | }
48 | };
49 |
50 | export const CreateNewDocument = async () => {
51 | try {
52 | const session = await getServerSession();
53 | if (session.id)
54 | return {
55 | success: false,
56 | error: "User is not logged in",
57 | };
58 |
59 | const doc = await prisma.document.create({
60 | data: {
61 | data: "",
62 | userId: session.id,
63 | users: {
64 | create: {
65 | user: {
66 | connect: {
67 | id: session.id,
68 | },
69 | },
70 | },
71 | },
72 | },
73 | });
74 |
75 | return { success: true, data: doc };
76 | } catch (e) {
77 | console.error(e);
78 | return { success: false, error: "Internal server error" };
79 | }
80 | };
81 |
82 | export const LogoutAction = async () => {
83 | try {
84 | cookies().delete("token");
85 | cookies().delete("next-auth.session-token");
86 |
87 | return { success: true, data: null };
88 | } catch (e) {
89 | console.error(e);
90 | return { success: false, error: "Internal server error" };
91 | }
92 | };
93 |
--------------------------------------------------------------------------------
/app/document/components/Header/components/HeaderButtons.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { useState } from "react";
4 | import { useRouter } from "next/navigation";
5 | import { toast } from "sonner";
6 | import { CloudUpload, PlusIcon } from "lucide-react";
7 |
8 | import { CreateNewDocument } from "../actions";
9 | import { createGuestDocument } from "@/lib/guestServices";
10 | import { Button } from "@/components/ui/button";
11 | import LoaderButton from "@/components/LoaderButton";
12 | import useClientSession from "@/lib/customHooks/useClientSession";
13 |
14 | export default function HeaderButtons() {
15 | const router = useRouter();
16 |
17 | const session = useClientSession();
18 |
19 | const [isLoading, setIsLoading] = useState(false);
20 |
21 | const createDocument = async () => {
22 | if (session?.id) {
23 | setIsLoading(true);
24 |
25 | const response = await CreateNewDocument();
26 | if (response.success) {
27 | setIsLoading(false);
28 | toast.success("Successfully created new document");
29 | router.push(`/writer/${response.data?.id}`);
30 | } else {
31 | setIsLoading(false);
32 | toast.error(response.error);
33 | }
34 | } else {
35 | const document = createGuestDocument();
36 | toast.success("Successfully created new document");
37 | router.push(`/writer/${document.id}`);
38 | }
39 | };
40 |
41 | return (
42 |
43 | {process.env.NODE_ENV === "development" ? (
44 |
48 |
49 | Upload
50 |
51 | ) : (
52 | <>>
53 | )}
54 |
}
59 | >
60 |
Create New
61 |
62 |
63 | );
64 | }
65 |
--------------------------------------------------------------------------------
/app/document/components/Header/components/ProfileBtn.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { useRouter } from "next/navigation";
4 | import { toast } from "sonner";
5 | import { LogIn, LogOut } from "lucide-react";
6 | import { PopoverContent, PopoverTrigger } from "@radix-ui/react-popover";
7 | import { motion } from "framer-motion";
8 |
9 | import { Avatar, AvatarImage } from "@/components/ui/avatar";
10 | import { Popover } from "@/components/ui/popover";
11 | import { Button } from "@/components/ui/button";
12 | import { SessionReturnType } from "@/lib/customHooks/ReturnType";
13 | import getInitials from "@/helpers/getInitials";
14 |
15 | import { LogoutAction } from "../actions";
16 | import useClientSession from "@/lib/customHooks/useClientSession";
17 |
18 | type ProfileBtnPropType = Pick;
19 | export default function ProfileBtn({ name, image }: ProfileBtnPropType) {
20 | const router = useRouter();
21 |
22 | const session = useClientSession();
23 |
24 | const logout = async () => {
25 | const response = await LogoutAction();
26 | if (response.success) {
27 | toast.success("Successfully logged out");
28 | router.push("/api/auth/signin");
29 | } else {
30 | toast.error(response.error);
31 | }
32 | };
33 | return (
34 | !session?.id
35 | ?
36 | router.push("/signup")}
40 | >
41 |
42 | Signup
43 |
44 |
45 | :
46 |
47 |
48 |
{getInitials(name ?? "X")}
49 |
50 | {image ? (
51 |
52 |
53 |
54 | ) : (
55 | <>>
56 | )}
57 |
58 |
59 |
69 |
74 |
75 | Logout
76 |
77 |
78 |
79 |
80 | );
81 | }
82 |
--------------------------------------------------------------------------------
/app/document/components/Header/components/SearchBar.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { useEffect, useRef, useState } from "react";
4 | import { useRouter } from "next/navigation";
5 | import { Search, X } from "lucide-react";
6 | import Image from "next/image";
7 |
8 | import { Input } from "@/components/ui/input";
9 | import prettifyDate from "@/helpers/prettifyDates";
10 | import doc from "@/public/output-onlinepngtools.svg";
11 | import useDebounce from "@/lib/customHooks/useDebounce";
12 |
13 | import { SearchDocAction } from "../actions";
14 | import useClientSession from "@/lib/customHooks/useClientSession";
15 |
16 | type SearchResultType = {
17 | id: string;
18 | name: string;
19 | updatedAt: Date;
20 | createdBy: {
21 | name: string;
22 | };
23 | };
24 | type SearchResponse = {
25 | success: boolean;
26 | data?: SearchResultType[];
27 | error?: string;
28 | };
29 | export default function SearchBar() {
30 | const router = useRouter();
31 |
32 | const debounce = useDebounce(async (value: string) => {
33 | if (!searchValue) return;
34 | setIsSearching(true);
35 | setSearchResponse(await SearchDocAction(value));
36 | setIsSearching(false);
37 | }, 500);
38 |
39 | const searchedResponseRef = useRef(null);
40 |
41 | const [searchResponse, setSearchResponse] = useState<
42 | SearchResponse | undefined
43 | >(undefined);
44 | const [searchValue, setSearchValue] = useState("");
45 | const [isFocused, setIsFocused] = useState(false);
46 | const [isSearching, setIsSearching] = useState(false);
47 |
48 | const handleDocumentClick = (e: any) => {
49 | if (
50 | searchedResponseRef.current &&
51 | !searchedResponseRef.current.contains(e.target)
52 | ) {
53 | setIsFocused(false);
54 | }
55 | };
56 | useEffect(() => {
57 | document.addEventListener("mousedown", handleDocumentClick);
58 | return () => {
59 | document.removeEventListener("mousedown", handleDocumentClick);
60 | };
61 | }, []);
62 |
63 | return (
64 |
67 |
68 | setIsFocused(true)}
74 | value={searchValue}
75 | onChange={(e) => {
76 | setSearchValue(e.target.value);
77 | if (!e.target.value) return setSearchResponse(undefined);
78 | // debouncedSearch(e.target.value);
79 | debounce(e.target.value);
80 | }}
81 | placeholder="Search documents..."
82 | />
83 |
87 | setSearchValue("")}
90 | className={`${!searchValue ? "hidden" : ""
91 | } absolute text-slate-500 right-0 top-1/2 transform -translate-y-1/2 mr-2 cursor-pointer`}
92 | />
93 |
94 |
95 |
100 | {isSearching ? (
101 |
102 |
114 |
115 |
116 | Searching
117 |
118 | ) : !searchResponse?.success ? (
119 |
120 | {searchResponse?.error}
121 |
122 | ) : (
123 | searchResponse.data?.map((data: SearchResultType) => {
124 | return (
125 |
router.push(`writer/${data.id}`)}
128 | className="z-50 flex cursor-pointer justify-between p-2 border-l-2 border-white hover:bg-neutral-100 hover:border-blue-500 "
129 | >
130 |
131 |
132 |
133 |
{data.name}
134 |
135 | {data.createdBy.name}
136 |
137 |
138 |
139 |
140 | {prettifyDate(String(data.updatedAt), {
141 | month: "short",
142 | day: "2-digit",
143 | })}
144 |
145 |
146 | );
147 | })
148 | )}
149 |
150 |
151 | );
152 | }
153 |
--------------------------------------------------------------------------------
/app/document/components/Onboarding.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import { useRouter } from "next/navigation";
4 | import { toast } from "sonner";
5 | import Image from "next/image";
6 |
7 | import { CreateNewDocument } from "./Header/actions";
8 | import { createGuestDocument } from "@/lib/guestServices";
9 | import EmptyBox from "@/public/createdoc.png"
10 | import useClientSession from "@/lib/customHooks/useClientSession";
11 |
12 | export default function Onboarding() {
13 | const router = useRouter();
14 |
15 | const session = useClientSession();
16 |
17 | const createDocument = async () => {
18 | if (session?.id) {
19 | const response = await CreateNewDocument();
20 | if (response.success) {
21 | toast.success("Successfully created new document");
22 | router.push(`/writer/${response.data?.id}`);
23 | } else {
24 | toast.error(response.error);
25 | }
26 | } else {
27 | const document = createGuestDocument();
28 | toast.success("Successfully created new document");
29 | router.push(`/writer/${document.id}`);
30 | }
31 | };
32 | return (
33 |
39 |
45 |
46 | )
47 | }
48 |
--------------------------------------------------------------------------------
/app/document/loading.tsx:
--------------------------------------------------------------------------------
1 | import { Card, CardFooter } from "@/components/ui/card";
2 | import Header from "./components/Header/Header";
3 |
4 | export default function Loading() {
5 | return (
6 | <>
7 |
8 |
9 | {[1, 2, 3, 4].map((i) => {
10 | return (
11 |
12 |
13 |
14 |
17 |
24 |
25 |
26 | );
27 | })}
28 |
29 | >
30 | );
31 | }
32 |
--------------------------------------------------------------------------------
/app/document/page.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import { useEffect, useState } from "react";
4 |
5 | import { GetAllDocs } from "./actions";
6 | import DocCard from "./components/Card/Card";
7 | import Header from "./components/Header/Header";
8 | import Onboarding from "./components/Onboarding";
9 | import useClientSession from "@/lib/customHooks/useClientSession";
10 | import { getAllGuestDocuments } from "@/lib/guestServices";
11 |
12 | export default function Home() {
13 | const session = useClientSession();
14 |
15 | const [data, setData] = useState(null);
16 |
17 | useEffect(() => {
18 | (async () => {
19 | if (session?.id) {
20 | const response = await GetAllDocs(session.id);
21 | if (response.success) {
22 | setData(response.data!);
23 | } else {
24 | setData([]);
25 | }
26 | } else {
27 | setData(getAllGuestDocuments());
28 | }
29 | })()
30 | }, [session.id])
31 |
32 | return (
33 |
34 |
35 | {data && data.length > 0 ? (
36 |
39 | {data.map((doc, index) => {
40 | return (
41 |
49 | );
50 | })}
51 |
52 | ) : (
53 |
54 | )}
55 |
56 | );
57 | }
58 |
--------------------------------------------------------------------------------
/app/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/git-init-priyanshu/Docx/74239b9d003fb588ce9402ed740566aaef0c15e3/app/favicon.ico
--------------------------------------------------------------------------------
/app/globals.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | @layer base {
6 | :root {
7 | --background: 0 0% 100%;
8 | --foreground: 222.2 84% 4.9%;
9 |
10 | --card: 0 0% 100%;
11 | --card-foreground: 222.2 84% 4.9%;
12 |
13 | --popover: 0 0% 100%;
14 | --popover-foreground: 222.2 84% 4.9%;
15 |
16 | --primary: 222.2 47.4% 11.2%;
17 | --primary-foreground: 210 40% 98%;
18 |
19 | --secondary: 210 40% 96.1%;
20 | --secondary-foreground: 222.2 47.4% 11.2%;
21 |
22 | --muted: 210 40% 96.1%;
23 | --muted-foreground: 215.4 16.3% 46.9%;
24 |
25 | --accent: 210 40% 96.1%;
26 | --accent-foreground: 222.2 47.4% 11.2%;
27 |
28 | --destructive: 0 84.2% 60.2%;
29 | --destructive-foreground: 210 40% 98%;
30 |
31 | --border: 214.3 31.8% 91.4%;
32 | --input: 214.3 31.8% 91.4%;
33 | --ring: 222.2 84% 4.9%;
34 |
35 | --radius: 0.5rem;
36 | }
37 |
38 | .dark {
39 | --background: 222.2 84% 4.9%;
40 | --foreground: 210 40% 98%;
41 |
42 | --card: 222.2 84% 4.9%;
43 | --card-foreground: 210 40% 98%;
44 |
45 | --popover: 222.2 84% 4.9%;
46 | --popover-foreground: 210 40% 98%;
47 |
48 | --primary: 210 40% 98%;
49 | --primary-foreground: 222.2 47.4% 11.2%;
50 |
51 | --secondary: 217.2 32.6% 17.5%;
52 | --secondary-foreground: 210 40% 98%;
53 |
54 | --muted: 217.2 32.6% 17.5%;
55 | --muted-foreground: 215 20.2% 65.1%;
56 |
57 | --accent: 217.2 32.6% 17.5%;
58 | --accent-foreground: 210 40% 98%;
59 |
60 | --destructive: 0 62.8% 30.6%;
61 | --destructive-foreground: 210 40% 98%;
62 |
63 | --border: 217.2 32.6% 17.5%;
64 | --input: 217.2 32.6% 17.5%;
65 | --ring: 212.7 26.8% 83.9%;
66 | }
67 | }
68 |
69 | @layer base {
70 | * {
71 | @apply border-border;
72 | }
73 | body {
74 | @apply bg-background text-foreground;
75 | }
76 | }
77 |
78 | .divider {
79 | display: flex;
80 | align-items: center;
81 | text-align: center;
82 | width: 100%;
83 | }
84 |
85 | .divider::before,
86 | .divider::after {
87 | content: "";
88 | flex: 1;
89 | border-bottom: 0.5px solid #808080;
90 | }
91 |
92 | .divider:not(:empty)::before {
93 | margin-right: 0.25em;
94 | }
95 |
96 | .divider:not(:empty)::after {
97 | margin-left: 0.25em;
98 | }
99 |
100 | /* Give a remote user a caret */
101 | .collaboration-cursor__caret {
102 | border-left: 1px solid #0d0d0d;
103 | border-right: 1px solid #0d0d0d;
104 | margin-left: -1px;
105 | margin-right: -1px;
106 | pointer-events: none;
107 | position: relative;
108 | word-break: normal;
109 | }
110 |
111 | /* Render the username above the caret */
112 | .collaboration-cursor__label {
113 | border-radius: 3px 3px 3px 0;
114 | color: #0d0d0d;
115 | font-size: 12px;
116 | font-style: normal;
117 | font-weight: 600;
118 | left: -1px;
119 | line-height: normal;
120 | padding: 0.1rem 0.3rem;
121 | position: absolute;
122 | top: -1.4em;
123 | user-select: none;
124 | white-space: nowrap;
125 | }
126 |
--------------------------------------------------------------------------------
/app/layout.tsx:
--------------------------------------------------------------------------------
1 | import { Analytics } from "@vercel/analytics/react";
2 | import type { Metadata } from "next";
3 | import { Inter } from "next/font/google";
4 | import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
5 |
6 | import { cn } from "@/lib/utils";
7 | import { Toaster } from "@/components/ui/sonner";
8 |
9 | import { NextAuthProvider } from "./NextAuthProvider";
10 | import ReactQueryProvider from "./ReactQueryProvider";
11 | import "./globals.css";
12 |
13 | const inter = Inter({ subsets: ["latin"] });
14 |
15 | export const metadata: Metadata = {
16 | title: "DocX",
17 | description:
18 | "An open-source alternative to Google Docs, that lets you write and customize your docs collaboratively with others",
19 | };
20 |
21 | export default function RootLayout({
22 | children,
23 | }: Readonly<{
24 | children: React.ReactNode;
25 | }>) {
26 | return (
27 |
28 |
29 |
30 |
36 |
37 |
38 | {children}
39 |
40 |
41 |
42 |
43 |
44 | );
45 | }
46 |
--------------------------------------------------------------------------------
/app/page.tsx:
--------------------------------------------------------------------------------
1 | import FeaturesSection from "./components/FeaturesSection";
2 | import HeroSection from "./components/HeroSection";
3 | import AboutSection from "./components/AboutSection";
4 | import ContactSection from "./components/ContactSection";
5 | import Footer from "./components/Footer";
6 |
7 | export default function Component() {
8 | return (
9 |
10 |
11 |
12 | {/* */}
13 | {/* */}
14 |
15 |
16 | );
17 | }
18 |
--------------------------------------------------------------------------------
/app/writer/[id]/actions.ts:
--------------------------------------------------------------------------------
1 | "use server";
2 |
3 | import { revalidatePath } from "next/cache";
4 | import { GoogleGenerativeAI } from "@google/generative-ai";
5 |
6 | import prisma from "@/prisma/prismaClient";
7 | import getServerSession from "@/lib/customHooks/getServerSession";
8 | import {
9 | generateTextOptions,
10 | prompts,
11 | } from "./components/BubbleMenuComp/generateTextConfig";
12 |
13 | export const GetDocDetails = async (id: any) => {
14 | try {
15 | const session = await getServerSession();
16 | if (!session.id)
17 | return {
18 | success: false,
19 | error: "User is not logged in",
20 | };
21 |
22 | const doc = await prisma.document.update({
23 | where: {
24 | id,
25 | },
26 | data: {
27 | users: {
28 | upsert: {
29 | where: {
30 | userId_documentId: {
31 | documentId: id,
32 | userId: session.id,
33 | },
34 | },
35 | update: {},
36 | create: {
37 | user: {
38 | connect: {
39 | id: session.id,
40 | },
41 | },
42 | },
43 | },
44 | },
45 | },
46 | });
47 | if (!doc)
48 | return {
49 | success: false,
50 | error: "Document does not exist",
51 | };
52 |
53 | return { success: true, data: doc };
54 | } catch (e) {
55 | console.log(e);
56 | return { success: false, error: "Internal server error" };
57 | }
58 | };
59 |
60 | export const UpdateDocData = async (id: any, data: string) => {
61 | const session = await getServerSession();
62 | if (!session.id)
63 | return {
64 | success: false,
65 | error: "User is not logged in",
66 | };
67 |
68 | try {
69 | const doc = await prisma.document.findFirst({
70 | where: {
71 | id,
72 | users: {
73 | some: { userId: session.id },
74 | },
75 | },
76 | });
77 | if (!doc)
78 | return {
79 | success: false,
80 | error: "Document does not exist",
81 | };
82 |
83 | await prisma.document.update({
84 | where: {
85 | id,
86 | users: {
87 | some: { userId: session.id },
88 | },
89 | },
90 | data: {
91 | data: data,
92 | updatedAt: new Date(),
93 | },
94 | });
95 |
96 | return { success: true, data: "Saved" };
97 | } catch (e) {
98 | console.log(e);
99 | return { success: false, error: "Internal server error" };
100 | }
101 | };
102 |
103 | export const UpdateThumbnail = async (id: any, thumbnail: string) => {
104 | try {
105 | const session = await getServerSession();
106 | if (!session.id)
107 | return {
108 | success: false,
109 | error: "User is not logged in",
110 | };
111 |
112 | const response = await fetch(
113 | `${process.env.BACKEND_SERVER_URL}/push-to-quque`,
114 | {
115 | method: "POST",
116 | headers: {
117 | "Content-Type": "application/json",
118 | },
119 | body: JSON.stringify({
120 | docId: id,
121 | thumbnail,
122 | userId: session.id,
123 | }),
124 | },
125 | );
126 | revalidatePath("/");
127 |
128 | return await response.json();
129 | } catch (e) {
130 | console.log(e);
131 | return { success: false, error: "Internal server error" };
132 | }
133 | };
134 |
135 | export const generateText = async (
136 | option: generateTextOptions,
137 | text: string,
138 | language?: string,
139 | ) => {
140 | try {
141 | const genAI = new GoogleGenerativeAI(process.env.GEMINI_API_KEY || "");
142 | const model = genAI.getGenerativeModel({ model: "gemini-1.5-flash" });
143 |
144 | let prompt: string;
145 | if (option === generateTextOptions.TRANSLATE) {
146 | if (!language) return { success: false, error: "Undefined prompt" };
147 | prompt = `Here is the text: ${text} and language: ${language}. ${prompts.find((e) => e.option === option)?.prompt}`;
148 | } else {
149 | prompt = `Here is the text: "${text}". ${prompts.find((e) => e.option === option)?.prompt}`;
150 | }
151 | if (!prompt) return { success: false, error: "Undefined prompt" };
152 |
153 | const note = "Note: Provide only the required text.";
154 | const result = await model.generateContent(prompt + note);
155 |
156 | return { success: true, data: result.response.text() };
157 | } catch (e) {
158 | console.log(e);
159 | return { success: false, error: "Internal server error" };
160 | }
161 | };
162 |
--------------------------------------------------------------------------------
/app/writer/[id]/components/BubbleMenuComp/generateTextConfig.ts:
--------------------------------------------------------------------------------
1 | export enum generateTextOptions {
2 | IMPROVE_WRITING = "improve_writing",
3 | FIX_SPELLINGS_AND_GRAMMAR = "fix_spellings_&_grammar",
4 | TRANSLATE = "translate",
5 | MAKE_LONGER = "make_longer",
6 | MAKE_SHORTER = "make_shorter",
7 | SIMPLIFY_LANGUAGE = "simplify_language",
8 | TRY_AGAIN = "try_again",
9 | }
10 |
11 | export const prompts = [
12 | {
13 | option: generateTextOptions.IMPROVE_WRITING,
14 | prompt: "Enhance the structure, clarity, and tone of the text.",
15 | },
16 | {
17 | option: generateTextOptions.FIX_SPELLINGS_AND_GRAMMAR,
18 | prompt: "Correct any spelling and grammatical mistakes in the text.",
19 | },
20 | {
21 | option: generateTextOptions.TRANSLATE,
22 | prompt: "Translate the given text to the above mentioned language.",
23 | },
24 | {
25 | option: generateTextOptions.MAKE_LONGER,
26 | prompt: "Expand the content to provide more details and depth.",
27 | },
28 | {
29 | option: generateTextOptions.MAKE_SHORTER,
30 | prompt: "Condense the text while keeping the key points intact.",
31 | },
32 | {
33 | option: generateTextOptions.SIMPLIFY_LANGUAGE,
34 | prompt: "Rewrite the text in simpler language to enhance readability.",
35 | },
36 | {
37 | option: generateTextOptions.TRY_AGAIN,
38 | prompt: "Rewrite the text with a new variation.",
39 | },
40 | ];
41 |
--------------------------------------------------------------------------------
/app/writer/[id]/components/BubbleMenuComp/index.tsx:
--------------------------------------------------------------------------------
1 | import { useState } from "react";
2 | import type { Editor } from "@tiptap/react";
3 |
4 | import AskAI from "./AskAI";
5 | import GeneratedText from "./GeneratedText";
6 | import ColorHighlight from "../options/format/ColorHighlight";
7 | import FormattingBtns from "../options/format/FormattingBtns";
8 |
9 | type BubbleMenuPropType = {
10 | editor: Editor | null;
11 | isHighlighted: boolean;
12 | bubblePosition: { x: number; y: number };
13 | generativeTextBubblePosition: { x: number; y: number; width: number };
14 | };
15 | export default function BubbleMenuComp({
16 | editor,
17 | isHighlighted,
18 | bubblePosition,
19 | generativeTextBubblePosition,
20 | }: BubbleMenuPropType) {
21 | const [isAiActive, setIsAiActive] = useState(false);
22 | const [isGeneratingText, setIsGeneratingText] = useState(false);
23 | const [generativeTextResult, setGenerativeTextResult] = useState("");
24 |
25 | if (!editor) return;
26 | return (
27 | <>
28 |
44 |
55 | >
56 | );
57 | }
58 |
--------------------------------------------------------------------------------
/app/writer/[id]/components/EditorLoading.tsx:
--------------------------------------------------------------------------------
1 | export default function Loading() {
2 | return (
3 |
4 |
5 |
6 |
7 |
11 |
12 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 | );
24 | }
25 |
--------------------------------------------------------------------------------
/app/writer/[id]/components/Header/Header.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { useRouter } from "next/navigation";
4 | import { ALargeSmall, Redo, Undo, X } from "lucide-react";
5 | import { Editor } from "@tiptap/react";
6 |
7 | import {
8 | Drawer,
9 | DrawerClose,
10 | DrawerContent,
11 | DrawerHeader,
12 | DrawerTitle,
13 | DrawerTrigger,
14 | } from "@/components/ui/drawer";
15 | import { Button } from "@/components/ui/button";
16 | import logo from "@/public/logo.svg";
17 |
18 | import Heading from "../options/format/Heading";
19 | import Font from "../options/format/Font";
20 | import FormattingBtns from "../options/format/FormattingBtns";
21 | import ColorHighlight from "../options/format/ColorHighlight";
22 | import ParagraphBtns from "../options/format/ParagraphBtns";
23 | import Image from "next/image";
24 |
25 | type HeaderPropType = {
26 | editor: Editor | null;
27 | name: string;
28 | isSaving: boolean;
29 | };
30 |
31 | export default function Header({ editor, name, isSaving }: HeaderPropType) {
32 | const router = useRouter();
33 |
34 | return (
35 |
133 | );
134 | }
135 |
--------------------------------------------------------------------------------
/app/writer/[id]/components/Tabs.tsx:
--------------------------------------------------------------------------------
1 | import { ALargeSmall, FileInput, LifeBuoy, SquareUser } from "lucide-react";
2 | import {
3 | Tooltip,
4 | TooltipContent,
5 | TooltipProvider,
6 | TooltipTrigger,
7 | } from "@/components/ui/tooltip";
8 | import { Button } from "@/components/ui/button";
9 |
10 | type TabsPropType = {
11 | option: number;
12 | setOption: React.Dispatch>;
13 | };
14 | export default function Tabs({ option, setOption }: TabsPropType) {
15 | return (
16 |
17 |
18 |
19 |
20 |
21 | setOption(0)}
27 | >
28 |
29 |
30 |
31 |
32 | Format
33 |
34 |
35 |
36 |
37 |
38 |
39 | setOption(1)}
45 | >
46 |
47 |
48 |
49 |
50 | Insert
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
65 |
66 |
67 |
68 |
69 | Help
70 |
71 |
72 |
73 |
74 |
75 |
76 |
82 |
83 |
84 |
85 |
86 | Account
87 |
88 |
89 |
90 |
91 |
92 | );
93 | }
94 |
--------------------------------------------------------------------------------
/app/writer/[id]/components/options/GoBack.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 | import { MoveLeft } from "lucide-react";
3 | import { useRouter } from "next/navigation";
4 |
5 | export default function GoBack() {
6 | const router = useRouter();
7 | return (
8 | router.push("/document")}
13 | />
14 | )
15 | }
16 |
--------------------------------------------------------------------------------
/app/writer/[id]/components/options/format/BulletListBtns.tsx:
--------------------------------------------------------------------------------
1 | import { List } from "lucide-react";
2 |
3 | import { BulletBtns } from "./textEditorOptions";
4 |
5 | export default function BulletListBtns({ editor }: any) {
6 | return (
7 |
8 | {BulletBtns.map(({ Icon, key }, i) => {
9 | return (
10 |
14 | Icon === List
15 | ? editor?.chain()?.focus().toggleBulletList().run()
16 | : editor?.chain().focus().toggleOrderedList().run()
17 | }
18 | className={`${
19 | Icon === List
20 | ? editor?.isActive("bulletList")
21 | ? "bg-blue-500 text-white hover:bg-blue-500"
22 | : "hover:bg-slate-100 bg-white"
23 | : editor?.isActive("orderedList")
24 | ? "bg-blue-500 text-white hover:bg-blue-500"
25 | : "hover:bg-slate-100 bg-white"
26 | } p-2 rounded ${
27 | i === BulletBtns.length - 1 ? "border-none" : "border-r"
28 | }`}
29 | />
30 | );
31 | })}
32 |
33 | );
34 | }
35 |
--------------------------------------------------------------------------------
/app/writer/[id]/components/options/format/ColorHighlight.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { useEffect, useRef, useState } from "react";
4 | import { Editor } from "@tiptap/react";
5 | import { Baseline, ChevronDown, Highlighter } from "lucide-react";
6 | import { HexColorPicker } from "react-colorful";
7 |
8 | import { Input } from "@/components/ui/input";
9 | import {
10 | DropdownMenu,
11 | DropdownMenuContent,
12 | DropdownMenuTrigger,
13 | } from "@/components/ui/dropdown";
14 |
15 | type ColorHighlightPropType = {
16 | editor: Editor | null;
17 | isBubbleMenuBtn: boolean;
18 | };
19 | export default function ColorHighlight({
20 | editor,
21 | isBubbleMenuBtn,
22 | }: ColorHighlightPropType) {
23 | const colorPopoverRef = useRef(null);
24 | const bgPopoverRef = useRef(null);
25 |
26 | const [isColorPopoverOpen, setIsColorPopoverOpen] = useState(false);
27 | const [isBgPopoverOpen, setIsBgPopoverOpen] = useState(false);
28 | const [fontColor, setFontColor] = useState("#000000");
29 | const [highlightColor, setHighlightColor] = useState("#fdfb7a");
30 |
31 | const onFontColorChange = (hex: string) => {
32 | if (!editor) return;
33 | setFontColor(hex);
34 | editor.chain().focus().setColor(hex).run();
35 | };
36 | const onHighlightColorChange = (hex: string) => {
37 | if (!editor) return;
38 | setHighlightColor(hex);
39 | editor.chain().focus().toggleHighlight({ color: hex }).run();
40 | };
41 |
42 | const handleDocumentClick = (e: any) => {
43 | if (
44 | (colorPopoverRef.current &&
45 | !colorPopoverRef.current.contains(e.target)) ||
46 | (bgPopoverRef.current && !bgPopoverRef.current.contains(e.target))
47 | ) {
48 | setIsColorPopoverOpen(false);
49 | setIsBgPopoverOpen(false);
50 | }
51 | };
52 | useEffect(() => {
53 | document.addEventListener("mousedown", handleDocumentClick);
54 | return () => {
55 | document.removeEventListener("mousedown", handleDocumentClick);
56 | };
57 | }, []);
58 |
59 | return (
60 |
134 | );
135 | }
136 |
--------------------------------------------------------------------------------
/app/writer/[id]/components/options/format/Font.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | Select,
3 | SelectContent,
4 | SelectItem,
5 | SelectTrigger,
6 | SelectValue,
7 | } from "@/components/ui/select";
8 |
9 | import { setDefaultFontFamily, onFontFamilyChange } from "./functions";
10 | import { fontFamily } from "./textEditorOptions";
11 |
12 | export default function Font({ editor }: any) {
13 | return (
14 | onFontFamilyChange(editor, value)}
17 | >
18 |
22 |
23 |
24 |
25 | {fontFamily.map((item) => {
26 | return (
27 |
28 | {item.title}
29 |
30 | );
31 | })}
32 |
33 |
34 | );
35 | }
36 |
--------------------------------------------------------------------------------
/app/writer/[id]/components/options/format/FormattingBtns.tsx:
--------------------------------------------------------------------------------
1 | import { formattingBtns } from "./textEditorOptions";
2 |
3 | type FormattingBtnsPropType = {
4 | editor: any;
5 | isBubbleMenuBtn: boolean;
6 | };
7 | export default function FormattingBtns({
8 | editor,
9 | isBubbleMenuBtn,
10 | }: FormattingBtnsPropType) {
11 | return (
12 |
15 | {formattingBtns.map(({ func, name, Icon }, i) => {
16 | return (
17 | editor?.chain().focus()[func]().run()}
21 | className={`${
22 | editor?.isActive(name)
23 | ? "bg-blue-500 text-white hover:bg-blue-500"
24 | : `hover:bg-slate-100 ${isBubbleMenuBtn ? "bg-neutral-50" : "bg-white"}`
25 | } p-2 rounded ${
26 | !isBubbleMenuBtn &&
27 | (i === formattingBtns.length - 1 ? "border-none" : "border-r")
28 | }`}
29 | >
30 |
31 |
32 | );
33 | })}
34 |
35 | );
36 | }
37 |
--------------------------------------------------------------------------------
/app/writer/[id]/components/options/format/Heading.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | Select,
3 | SelectContent,
4 | SelectItem,
5 | SelectTrigger,
6 | SelectValue,
7 | } from "@/components/ui/select";
8 |
9 | import { setDefaultStyleValue, onFontStyleChange } from "./functions";
10 |
11 | export default function Heading({ editor }: any) {
12 | return (
13 | onFontStyleChange(editor, value)}
16 | >
17 |
21 |
22 |
23 |
24 | Normal
25 | Heading 1
26 | Heading 2
27 | Heading 3
28 | Heading 4
29 | Heading 5
30 | Heading 6
31 |
32 |
33 | );
34 | }
35 |
--------------------------------------------------------------------------------
/app/writer/[id]/components/options/format/ParagraphBtns.tsx:
--------------------------------------------------------------------------------
1 | import { paragraphBtns } from "./textEditorOptions";
2 |
3 | export default function ParagraphBtns({ editor }: any) {
4 | return (
5 |
6 | {paragraphBtns.map(({ align, Icon }, i) => {
7 | return (
8 | editor?.chain().focus().setTextAlign(align).run()}
12 | className={`${
13 | editor?.isActive({ textAlign: align })
14 | ? "bg-blue-500 text-white hover:bg-blue-500"
15 | : "hover:bg-slate-100 bg-white"
16 | } p-2 rounded ${
17 | i === paragraphBtns.length - 1 ? "border-none" : "border-r"
18 | }`}
19 | />
20 | );
21 | })}
22 |
23 | );
24 | }
25 |
--------------------------------------------------------------------------------
/app/writer/[id]/components/options/format/functions.ts:
--------------------------------------------------------------------------------
1 | import { fontFamily } from "./textEditorOptions";
2 |
3 | export const setDefaultStyleValue = (editor: any) => {
4 | let level: string = "normal";
5 | for (let i = 1; i <= 6; i++) {
6 | if (editor?.isActive("heading", { level: i }))
7 | return (level = `heading ${String(i)}`);
8 | }
9 | return level;
10 | };
11 |
12 | export const onFontStyleChange = (editor: any, val: string) => {
13 | const matcher = val.split(" ");
14 | if (matcher[0] === "normal") return editor?.commands.setParagraph();
15 | // @ts-ignore
16 | return editor?.commands.setHeading({ level: Number(matcher[1]) });
17 | };
18 |
19 | export const setDefaultFontFamily = (editor: any) => {
20 | const matcher = fontFamily.find((item) => {
21 | return editor?.isActive("textStyle", { fontFamily: item.font });
22 | });
23 | return matcher?.font || "Inter";
24 | };
25 |
26 | export const onFontFamilyChange = (editor: any, font: string) => {
27 | return editor?.chain().focus().setFontFamily(font).run();
28 | };
29 |
--------------------------------------------------------------------------------
/app/writer/[id]/components/options/format/index.tsx:
--------------------------------------------------------------------------------
1 | import type { Editor } from "@tiptap/react";
2 |
3 | import Heading from "./Heading";
4 | import Font from "./Font";
5 | import FormattingBtns from "./FormattingBtns";
6 | import ColorHighlight from "./ColorHighlight";
7 | import ParagraphBtns from "./ParagraphBtns";
8 | import BulletListBtns from "./BulletListBtns";
9 | import GoBack from "../GoBack";
10 |
11 | export default function FormatOptions({ editor }: { editor: Editor | null }) {
12 | return (
13 |
44 | );
45 | }
46 |
--------------------------------------------------------------------------------
/app/writer/[id]/components/options/format/textEditorOptions.ts:
--------------------------------------------------------------------------------
1 | import {
2 | AlignCenter,
3 | AlignJustify,
4 | AlignLeft,
5 | AlignRight,
6 | Bold,
7 | Italic,
8 | List,
9 | ListOrdered,
10 | Strikethrough,
11 | Underline,
12 | } from "lucide-react";
13 |
14 | export const formattingBtns = [
15 | { Icon: Bold, name: "bold", func: "toggleBold" },
16 | { Icon: Italic, name: "italic", func: "toggleItalic" },
17 | { Icon: Underline, name: "underline", func: "toggleUnderline" },
18 | { Icon: Strikethrough, name: "strike", func: "toggleStrike" },
19 | ];
20 | export const paragraphBtns = [
21 | { Icon: AlignLeft, align: "left" },
22 | { Icon: AlignCenter, align: "center" },
23 | { Icon: AlignRight, align: "right" },
24 | { Icon: AlignJustify, align: "justify" },
25 | ];
26 | export const BulletBtns = [
27 | { Icon: List, key: "list" },
28 | { Icon: ListOrdered, key: "ordered_list" },
29 | ];
30 | export const fontFamily = [
31 | { title: "Inter", font: "Inter" },
32 | { title: "Comic Sans", font: "Comic Sans MS, Comic Sans" },
33 | { title: "Serif", font: "serif" },
34 | { title: "Monospace", font: "monospace" },
35 | { title: "Cursive", font: "cursive" },
36 | ];
37 |
--------------------------------------------------------------------------------
/app/writer/[id]/components/options/index.ts:
--------------------------------------------------------------------------------
1 | import FormatOptions from "./format";
2 | import InsertOptions from "./insert";
3 |
4 | export { FormatOptions, InsertOptions };
5 |
--------------------------------------------------------------------------------
/app/writer/[id]/components/options/insert/index.tsx:
--------------------------------------------------------------------------------
1 | import { Editor } from "@tiptap/react";
2 | import GoBack from "../GoBack";
3 |
4 | type InsterOptionsPropTypes = {
5 | editor: Editor | null;
6 | };
7 | export default function InsertOptions({ editor }: InsterOptionsPropTypes) {
8 | return (
9 |
38 | );
39 | }
40 |
--------------------------------------------------------------------------------
/app/writer/[id]/editor/editorConfig.ts:
--------------------------------------------------------------------------------
1 | import { Color } from "@tiptap/extension-color";
2 | import { WebsocketProvider } from "y-websocket";
3 | import StarterKit from "@tiptap/starter-kit";
4 | import Highlight from "@tiptap/extension-highlight";
5 | import Underline from "@tiptap/extension-underline";
6 | import TextAlign from "@tiptap/extension-text-align";
7 | import FontFamily from "@tiptap/extension-font-family";
8 | import TextStyle from "@tiptap/extension-text-style";
9 | import * as Y from "yjs";
10 |
11 | import { cn } from "@/lib/utils";
12 |
13 | export const ydoc = new Y.Doc();
14 |
15 | const room = `room.${new Date()
16 | .getFullYear()
17 | .toString()
18 | .slice(-2)}${new Date().getMonth() + 1}${new Date().getDate()}`;
19 |
20 | export const provider = new WebsocketProvider(
21 | process.env.NEXT_PUBLIC_WEBSOCKET_URL as string,
22 | room,
23 | ydoc,
24 | );
25 |
26 | export const extensions = [
27 | StarterKit.configure({
28 | history: false,
29 | heading: {
30 | levels: [1, 2, 3, 4, 5, 6],
31 | },
32 | }),
33 | Color.configure({ types: [TextStyle.name] }),
34 | Highlight.configure({ multicolor: true }),
35 | Underline,
36 | TextStyle,
37 | FontFamily,
38 | TextAlign.configure({
39 | types: ["heading", "paragraph"],
40 | }),
41 | ];
42 |
43 | export const props = {
44 | attributes: {
45 | class: cn(
46 | "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",
47 | ),
48 | },
49 | };
50 |
--------------------------------------------------------------------------------
/app/writer/[id]/editor/index.ts:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { useEffect, useState, useCallback } from "react";
4 | import { useParams } from "next/navigation";
5 | import { useEditor } from "@tiptap/react";
6 | import { toast } from "sonner";
7 | import type { Document } from "@prisma/client";
8 | import html2canvas from "html2canvas";
9 | import Collaboration from "@tiptap/extension-collaboration";
10 | import CollaborationCursor from "@tiptap/extension-collaboration-cursor";
11 |
12 | import { getRandomColor } from "@/helpers/getRandomColor";
13 | import { getGuestDocumentDetails, updateGuestDocument } from "@/lib/guestServices";
14 | import useClientSession from "@/lib/customHooks/useClientSession";
15 | import useDebounce from "@/lib/customHooks/useDebounce";
16 |
17 | import { ydoc, provider, extensions, props } from "./editorConfig";
18 | import { GetDocDetails, UpdateDocData, UpdateThumbnail } from "../actions";
19 |
20 | type EditorPropType = {
21 | setIsSaving: React.Dispatch>;
22 | };
23 | export const Editor = ({ setIsSaving }: EditorPropType) => {
24 | const params = useParams();
25 |
26 | const session = useClientSession();
27 |
28 | const [name, setName] = useState("");
29 | const [docData, setDocData] = useState(undefined);
30 | const [status, setStatus] = useState("connecting");
31 | // console.log(status);
32 |
33 | useEffect(() => {
34 | setName(localStorage.getItem("name") || "");
35 | }, []);
36 |
37 | useEffect(() => {
38 | // Update status changes
39 | const statusHandler = (event: any) => {
40 | setStatus(event.status);
41 | };
42 |
43 | provider.on("status", statusHandler);
44 |
45 | return () => {
46 | provider.off("status", statusHandler);
47 | };
48 | }, []);
49 |
50 | // Doc data fetching
51 | useEffect(() => {
52 | if (session?.id) {
53 | (async () => {
54 | const response = await GetDocDetails(params.id);
55 | if (response.success) {
56 | setDocData(response.data);
57 | } else {
58 | toast.error(response.error);
59 | }
60 | })();
61 | } else {
62 | const data = getGuestDocumentDetails(params.id as string)
63 | setDocData(data);
64 | }
65 | }, [session?.id, params.id]);
66 |
67 | const createDocThumbnail = useCallback(async () => {
68 | try {
69 | const page = document.getElementsByClassName("tiptap")[0];
70 | if (!page) return setIsSaving(false);
71 | // @ts-ignore
72 | const canvas = await html2canvas(page, { scale: 1 });
73 |
74 | const thumbnail = canvas
75 | .toDataURL(`${docData?.id}thumbnail/png`)
76 | .replace(/^data:image\/\w+;base64,/, "");
77 |
78 | if (session?.id) {
79 | await UpdateThumbnail(params.id, thumbnail);
80 | } else {
81 | updateGuestDocument(params.id as string, "thumbnail", thumbnail);
82 | }
83 | setIsSaving(false);
84 | } catch (e) {
85 | console.log(e);
86 | toast.error("Something went wrong");
87 | }
88 | }, [docData?.id, params.id]);
89 |
90 | const debounce = useDebounce(async (editor: any) => {
91 | setIsSaving(true);
92 |
93 | if (session?.id) {
94 | const response = await UpdateDocData(
95 | params.id as string,
96 | JSON.stringify(editor.getJSON()),
97 | );
98 | if (response.success) {
99 | setIsSaving(false);
100 | return createDocThumbnail();
101 | }
102 | setIsSaving(false);
103 | toast.error(response.error);
104 | } else {
105 | updateGuestDocument(
106 | params.id as string,
107 | "data",
108 | JSON.stringify(editor.getJSON()),
109 | )
110 | createDocThumbnail();
111 | setIsSaving(false);
112 | }
113 | }, 1000);
114 |
115 | // Editor instance
116 | const editor = useEditor({
117 | onCreate: ({ editor: currentEditor }) => {
118 | provider.on("sync", () => {
119 | if (currentEditor.isEmpty) {
120 | currentEditor.commands.setContent("");
121 | }
122 | });
123 | },
124 | extensions: [
125 | ...extensions,
126 | Collaboration.configure({
127 | document: ydoc,
128 | }),
129 | CollaborationCursor.configure({
130 | provider,
131 | user: {
132 | name,
133 | color: getRandomColor(),
134 | },
135 | }),
136 | ],
137 | editorProps: props,
138 | content: "",
139 | onUpdate({ editor }) {
140 | debounce(editor);
141 | },
142 | });
143 |
144 | // Save current user to localStorage and emit to editor
145 | useEffect(() => {
146 | if (editor) {
147 | editor.chain().focus().updateUser({ name }).run();
148 | }
149 | }, [editor, name]);
150 |
151 | // Set content of the doc
152 | useEffect(() => {
153 | if (editor && docData) {
154 | editor.commands.setContent(
155 | docData?.data ? JSON.parse(docData?.data) : "",
156 | );
157 | }
158 | }, [editor, docData, docData?.data]);
159 |
160 | return { editor, docData };
161 | };
162 |
--------------------------------------------------------------------------------
/app/writer/[id]/page.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { useEffect, useState } from "react";
4 | import { EditorContent } from "@tiptap/react";
5 |
6 | import { ScrollArea } from "@/components/ui/scroll-area";
7 |
8 | import { Editor } from "./editor";
9 | import { FormatOptions, InsertOptions } from "./components/options";
10 | import Header from "./components/Header/Header";
11 | import Tabs from "./components/Tabs";
12 | import Loading from "./components/EditorLoading";
13 | import BubbleMenuComp from "./components/BubbleMenuComp";
14 |
15 | export default function Dashboard() {
16 | const [option, setOption] = useState(0);
17 | const [isSaving, setIsSaving] = useState(false);
18 | const [isHighlighted, setIsHighlighted] = useState(false);
19 | const [position, setPosition] = useState({ x: 0, y: 0 });
20 | const [position2, setPosition2] = useState({ x: 0, y: 0, width: 0 });
21 |
22 | const { editor, docData } = Editor({ setIsSaving });
23 |
24 | const Options = [
25 | ,
26 | ,
27 | ];
28 |
29 | // For bubble menu
30 | useEffect(() => {
31 | const handleSelectionChange = () => {
32 | const selection = window.getSelection();
33 | if (!selection) return;
34 |
35 | setIsHighlighted(selection && selection.toString().length > 0);
36 |
37 | const editorDimensions = document
38 | .getElementsByClassName("tiptap")[0]
39 | .getBoundingClientRect();
40 | const selectionDimensions = selection
41 | .getRangeAt(0)
42 | .getBoundingClientRect();
43 | setPosition({
44 | x:
45 | selectionDimensions.left -
46 | editorDimensions.left +
47 | selectionDimensions.width / 2,
48 | y: selectionDimensions.top - editorDimensions.top - 60,
49 | });
50 | setPosition2({
51 | x:
52 | selectionDimensions.left -
53 | editorDimensions.left +
54 | selectionDimensions.width / 2,
55 | y:
56 | selectionDimensions.top -
57 | editorDimensions.top +
58 | selectionDimensions.height +
59 | 20,
60 | width: selectionDimensions.width,
61 | });
62 | };
63 |
64 | document.addEventListener("selectionchange", handleSelectionChange);
65 |
66 | return () => {
67 | document.removeEventListener("selectionchange", handleSelectionChange);
68 | };
69 | }, []);
70 |
71 | return (
72 |
73 |
74 |
75 |
76 |
77 | {Options[option]}
78 |
79 | {!docData ? (
80 |
81 | ) : (
82 | <>
83 |
89 |
90 | >
91 | )}
92 |
93 |
94 |
95 |
96 | );
97 | }
98 |
--------------------------------------------------------------------------------
/components.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://ui.shadcn.com/schema.json",
3 | "style": "default",
4 | "rsc": true,
5 | "tsx": true,
6 | "tailwind": {
7 | "config": "tailwind.config.ts",
8 | "css": "app/globals.css",
9 | "baseColor": "slate",
10 | "cssVariables": true,
11 | "prefix": ""
12 | },
13 | "aliases": {
14 | "components": "@/components",
15 | "utils": "@/lib/utils"
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/components/AvatarList.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | import { Avatar, AvatarImage } from "@/components/ui/avatar";
4 | import type { User } from "@prisma/client";
5 | import getInitials from "@/helpers/getInitials";
6 |
7 | type AvatarListPropType = {
8 | users: {
9 | user: Pick;
10 | }[];
11 | };
12 |
13 | export default function AvatarList({ users }: AvatarListPropType) {
14 | return (
15 |
16 | {users.map((e, index) => {
17 | return (
18 |
19 |
20 |
{getInitials(e.user.name ?? "X")}
21 |
22 | {e.user.picture ? (
23 |
24 |
25 |
26 | ) : (
27 | <>>
28 | )}
29 |
30 | );
31 | })}
32 |
33 | );
34 | }
35 |
--------------------------------------------------------------------------------
/components/GridBg.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { useEffect, useState } from "react";
4 | import { motion } from "framer-motion";
5 |
6 | type GridBgPropType = {
7 | customClass?: string,
8 | isCursorMaskEnabled: boolean,
9 | cursorMaskSize: number;
10 | bgMaskSize: number;
11 | bgMaskPos: [number, number]
12 | blurValue: number;
13 | opacity: number;
14 | }
15 | export default function GridBg({
16 | customClass = "",
17 | isCursorMaskEnabled,
18 | cursorMaskSize,
19 | bgMaskSize,
20 | bgMaskPos,
21 | blurValue,
22 | opacity
23 | }: GridBgPropType) {
24 | const commonClass = "absolute w-full h-full";
25 | const commonStyle = {
26 | background: "url('/grid_bg.svg')",
27 | backgroundSize: "200px",
28 | WebkitMaskImage: "url('/mask.svg')",
29 | WebkitMaskRepeat: "no-repeat",
30 | filter: `blur(${blurValue}px)`,
31 | };
32 |
33 | const [cursorPosX, setCursorPosX] = useState(0);
34 | const [cursorPosY, setCursorPosY] = useState(0);
35 |
36 | const handleMouseMove = (e: MouseEvent) => {
37 | setCursorPosX(e.clientX);
38 | setCursorPosY(e.pageY);
39 | };
40 | useEffect(() => {
41 | window.addEventListener("mousemove", handleMouseMove);
42 |
43 | return () => {
44 | window.removeEventListener("mousemove", handleMouseMove);
45 | };
46 | }, []);
47 |
48 | return (
49 | <>
50 | {/* Cursor following mask */}
51 | {isCursorMaskEnabled &&
52 |
66 | }
67 | {/* Fixed mask */}
68 |
81 | >
82 | );
83 | }
84 |
--------------------------------------------------------------------------------
/components/HeaderBtn.tsx:
--------------------------------------------------------------------------------
1 | import { Button, ButtonProps } from "./ui/button";
2 |
3 | type HeaderBtnPropType = {
4 | icon?: React.ReactNode;
5 | size?: number;
6 | } & ButtonProps;
7 |
8 | export default function HeaderBtn({
9 | icon,
10 | size,
11 | children,
12 | ...props
13 | }: HeaderBtnPropType) {
14 | return {children} ;
15 | }
16 |
--------------------------------------------------------------------------------
/components/LoaderButton.tsx:
--------------------------------------------------------------------------------
1 | import { Button, ButtonProps } from "./ui/button";
2 |
3 | type LoaderButtonType = {
4 | onClickFunc?: (() => void) | (() => Promise);
5 | isLoading: boolean;
6 | className: string;
7 | children: React.ReactNode;
8 | icon?: React.ReactNode;
9 | } & ButtonProps;
10 |
11 | export default function LoaderButton({
12 | onClickFunc,
13 | isLoading,
14 | className,
15 | children,
16 | icon,
17 | ...props
18 | }: LoaderButtonType) {
19 | return (
20 |
26 | {isLoading ? (
27 |
39 |
40 |
41 | ) : (
42 | icon
43 | )}
44 | {children}
45 |
46 | );
47 | }
48 |
--------------------------------------------------------------------------------
/components/LoopWords.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { motion, AnimatePresence } from "framer-motion";
4 | import { useEffect, useState } from "react";
5 |
6 | type LoopWordsPropType = {
7 | className?: string;
8 | words: string[];
9 | };
10 |
11 | export default function LoopWords({ className, words }: LoopWordsPropType) {
12 | const [currentWord, setCurrentWord] = useState(0);
13 |
14 | useEffect(() => {
15 | const interval = setInterval(() => {
16 | setCurrentWord((curr) => (curr + 1) % words.length);
17 | }, 2000);
18 |
19 | return () => clearInterval(interval);
20 | }, [words]);
21 |
22 | // const letterVariants = {
23 | // hidden: { opacity: 0, y: 20 },
24 | // visible: { opacity: 1, y: 0 },
25 | // exit: { opacity: 0, y: -20 }
26 | // };
27 |
28 | return (
29 |
30 |
47 | {words[currentWord].split("").map((char, i) => {
48 | return (
49 |
63 | {char}
64 |
65 | );
66 | })}
67 |
68 |
69 | );
70 | }
71 |
--------------------------------------------------------------------------------
/components/Navbar.tsx:
--------------------------------------------------------------------------------
1 | import Link from "next/link";
2 | import Image from "next/image";
3 | import * as motion from "framer-motion/client";
4 | import { Montserrat_Alternates as Montserrat } from "next/font/google";
5 |
6 | import logo from "@/public/logo.svg";
7 | import Github from "@/public/github.png";
8 | import { GithubIcon } from "lucide-react";
9 |
10 | const roboto = Montserrat({
11 | weight: "500",
12 | style: "normal",
13 | subsets: ["cyrillic"],
14 | });
15 | export default function Navbar() {
16 | return (
17 | <>
18 |
28 |
29 |
36 |
37 |
38 |
54 | DocX
55 |
56 |
57 |
58 |
68 |
73 | {/* */}
74 |
75 | 11 stars
76 |
77 | {/* */}
82 | {/* */}
83 | {/* */}
84 |
85 |
86 | >
87 | );
88 | }
89 |
90 | const NavVariant = {
91 | hidden: {
92 | y: 10,
93 | opacity: 0,
94 | },
95 | visible: {
96 | y: 0,
97 | opacity: 1,
98 | },
99 | };
100 |
--------------------------------------------------------------------------------
/components/ScrollCard.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 | import React, { useRef } from "react";
3 | import { useScroll, useTransform, motion, MotionValue } from "framer-motion";
4 |
5 | export const ScrollCard = ({ children }: { children: React.ReactNode }) => {
6 | const containerRef = useRef(null);
7 | const { scrollYProgress } = useScroll({
8 | target: containerRef,
9 | });
10 | const [isMobile, setIsMobile] = React.useState(false);
11 |
12 | React.useEffect(() => {
13 | const checkMobile = () => {
14 | setIsMobile(window.innerWidth <= 768);
15 | };
16 | checkMobile();
17 | window.addEventListener("resize", checkMobile);
18 | return () => {
19 | window.removeEventListener("resize", checkMobile);
20 | };
21 | }, []);
22 |
23 | const scaleDimensions = () => {
24 | return isMobile ? [0.7, 0.9] : [1.05, 1];
25 | };
26 |
27 | const rotate = useTransform(scrollYProgress, [0, 1], [20, 0]);
28 | const scale = useTransform(scrollYProgress, [0, 1], scaleDimensions());
29 | const translate = useTransform(scrollYProgress, [0, 1], [0, -100]);
30 |
31 | return (
32 |
36 |
42 |
43 | {children}
44 |
45 |
46 |
47 | );
48 | };
49 |
50 | export const Header = ({ translate, titleComponent }: any) => {
51 | return (
52 |
58 | {titleComponent}
59 |
60 | );
61 | };
62 |
63 | export const Card = ({
64 | rotate,
65 | scale,
66 | children,
67 | }: {
68 | rotate: MotionValue;
69 | scale: MotionValue;
70 | translate: MotionValue;
71 | children: React.ReactNode;
72 | }) => {
73 | return (
74 |
83 |
84 | {children}
85 |
86 |
87 | );
88 | };
89 |
--------------------------------------------------------------------------------
/components/aiAnimation/components/ColorHighlight.tsx:
--------------------------------------------------------------------------------
1 | import { Baseline, ChevronDown, Highlighter } from "lucide-react";
2 |
3 | type ColorHighlightPropType = {
4 | isBubbleMenuBtn: boolean;
5 | };
6 | export default function ColorHighlight({ isBubbleMenuBtn }: ColorHighlightPropType) {
7 | return (
8 |
11 |
15 |
20 |
25 |
30 |
31 | )
32 | }
33 |
--------------------------------------------------------------------------------
/components/aiAnimation/components/FormattingBtns.tsx:
--------------------------------------------------------------------------------
1 | import { Bold, Italic, Strikethrough, Underline } from "lucide-react";
2 |
3 | const formattingBtns = [
4 | { Icon: Bold, name: "bold", func: "toggleBold" },
5 | { Icon: Italic, name: "italic", func: "toggleItalic" },
6 | { Icon: Underline, name: "underline", func: "toggleUnderline" },
7 | { Icon: Strikethrough, name: "strike", func: "toggleStrike" },
8 | ];
9 | type FormattingBtnsPropType = {
10 | isBubbleMenuBtn: boolean;
11 | };
12 | export default function FormattingBtns({
13 | isBubbleMenuBtn,
14 | }: FormattingBtnsPropType) {
15 | return (
16 |
19 | {formattingBtns.map(({ name, Icon }, i) => {
20 | return (
21 |
30 |
31 |
32 | );
33 | })}
34 |
35 | );
36 | }
37 |
--------------------------------------------------------------------------------
/components/ui/avatar.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as React from "react";
4 | import * as AvatarPrimitive from "@radix-ui/react-avatar";
5 |
6 | import { cn } from "@/lib/utils";
7 |
8 | const Avatar = React.forwardRef<
9 | React.ElementRef,
10 | React.ComponentPropsWithoutRef
11 | >(({ className, ...props }, ref) => (
12 |
20 | ));
21 | Avatar.displayName = AvatarPrimitive.Root.displayName;
22 |
23 | const AvatarImage = React.forwardRef<
24 | React.ElementRef,
25 | React.ComponentPropsWithoutRef
26 | >(({ className, ...props }, ref) => (
27 |
32 | ));
33 | AvatarImage.displayName = AvatarPrimitive.Image.displayName;
34 |
35 | const AvatarFallback = React.forwardRef<
36 | React.ElementRef,
37 | React.ComponentPropsWithoutRef
38 | >(({ className, ...props }, ref) => (
39 |
47 | ));
48 | AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName;
49 |
50 | export { Avatar, AvatarImage, AvatarFallback };
51 |
--------------------------------------------------------------------------------
/components/ui/badge.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import { cva, type VariantProps } from "class-variance-authority";
3 |
4 | import { cn } from "@/lib/utils";
5 |
6 | const badgeVariants = cva(
7 | "inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
8 | {
9 | variants: {
10 | variant: {
11 | default:
12 | "border-transparent bg-primary text-primary-foreground hover:bg-primary/80",
13 | secondary:
14 | "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
15 | destructive:
16 | "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",
17 | outline: "text-foreground",
18 | },
19 | },
20 | defaultVariants: {
21 | variant: "default",
22 | },
23 | },
24 | );
25 |
26 | export interface BadgeProps
27 | extends React.HTMLAttributes,
28 | VariantProps {}
29 |
30 | function Badge({ className, variant, ...props }: BadgeProps) {
31 | return (
32 |
33 | );
34 | }
35 |
36 | export { Badge, badgeVariants };
37 |
--------------------------------------------------------------------------------
/components/ui/button.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import { Slot } from "@radix-ui/react-slot";
3 | import { cva, type VariantProps } from "class-variance-authority";
4 |
5 | import { cn } from "@/lib/utils";
6 |
7 | const buttonVariants = cva(
8 | "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
9 | {
10 | variants: {
11 | variant: {
12 | default: "bg-primary text-primary-foreground hover:bg-primary/90",
13 | destructive:
14 | "bg-destructive text-destructive-foreground hover:bg-destructive/90",
15 | outline:
16 | "border border-input bg-background hover:bg-accent hover:text-accent-foreground",
17 | secondary:
18 | "bg-secondary text-secondary-foreground hover:bg-secondary/80",
19 | ghost: "hover:bg-accent hover:text-accent-foreground",
20 | link: "text-primary underline-offset-4 hover:underline",
21 | },
22 | size: {
23 | default: "h-10 px-4 py-2",
24 | sm: "h-9 rounded-md px-3",
25 | lg: "h-11 rounded-md px-8",
26 | icon: "h-10 w-10",
27 | },
28 | },
29 | defaultVariants: {
30 | variant: "default",
31 | size: "default",
32 | },
33 | },
34 | );
35 |
36 | export interface ButtonProps
37 | extends React.ButtonHTMLAttributes,
38 | VariantProps {
39 | asChild?: boolean;
40 | }
41 |
42 | const Button = React.forwardRef(
43 | ({ className, variant, size, asChild = false, ...props }, ref) => {
44 | const Comp = asChild ? Slot : "button";
45 | return (
46 |
51 | );
52 | },
53 | );
54 | Button.displayName = "Button";
55 |
56 | export { Button, buttonVariants };
57 |
--------------------------------------------------------------------------------
/components/ui/card.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 |
3 | import { cn } from "@/lib/utils";
4 |
5 | const Card = React.forwardRef<
6 | HTMLDivElement,
7 | React.HTMLAttributes
8 | >(({ className, ...props }, ref) => (
9 |
17 | ));
18 | Card.displayName = "Card";
19 |
20 | const CardHeader = React.forwardRef<
21 | HTMLDivElement,
22 | React.HTMLAttributes
23 | >(({ className, ...props }, ref) => (
24 |
29 | ));
30 | CardHeader.displayName = "CardHeader";
31 |
32 | const CardTitle = React.forwardRef<
33 | HTMLParagraphElement,
34 | React.HTMLAttributes
35 | >(({ className, ...props }, ref) => (
36 |
44 | ));
45 | CardTitle.displayName = "CardTitle";
46 |
47 | const CardDescription = React.forwardRef<
48 | HTMLParagraphElement,
49 | React.HTMLAttributes
50 | >(({ className, ...props }, ref) => (
51 |
56 | ));
57 | CardDescription.displayName = "CardDescription";
58 |
59 | const CardContent = React.forwardRef<
60 | HTMLDivElement,
61 | React.HTMLAttributes
62 | >(({ className, ...props }, ref) => (
63 |
64 | ));
65 | CardContent.displayName = "CardContent";
66 |
67 | const CardFooter = React.forwardRef<
68 | HTMLDivElement,
69 | React.HTMLAttributes
70 | >(({ className, ...props }, ref) => (
71 |
76 | ));
77 | CardFooter.displayName = "CardFooter";
78 |
79 | export {
80 | Card,
81 | CardHeader,
82 | CardFooter,
83 | CardTitle,
84 | CardDescription,
85 | CardContent,
86 | };
87 |
--------------------------------------------------------------------------------
/components/ui/dialog.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as React from "react";
4 | import * as DialogPrimitive from "@radix-ui/react-dialog";
5 | import { X } from "lucide-react";
6 |
7 | import { cn } from "@/lib/utils";
8 |
9 | const Dialog = DialogPrimitive.Root;
10 |
11 | const DialogTrigger = DialogPrimitive.Trigger;
12 |
13 | const DialogPortal = DialogPrimitive.Portal;
14 |
15 | const DialogClose = DialogPrimitive.Close;
16 |
17 | const DialogOverlay = React.forwardRef<
18 | React.ElementRef,
19 | React.ComponentPropsWithoutRef
20 | >(({ className, ...props }, ref) => (
21 |
29 | ));
30 | DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;
31 |
32 | const DialogContent = React.forwardRef<
33 | React.ElementRef,
34 | React.ComponentPropsWithoutRef
35 | >(({ className, children, ...props }, ref) => (
36 |
37 |
38 |
46 | {children}
47 |
48 |
49 | Close
50 |
51 |
52 |
53 | ));
54 | DialogContent.displayName = DialogPrimitive.Content.displayName;
55 |
56 | const DialogHeader = ({
57 | className,
58 | ...props
59 | }: React.HTMLAttributes) => (
60 |
67 | );
68 | DialogHeader.displayName = "DialogHeader";
69 |
70 | const DialogFooter = ({
71 | className,
72 | ...props
73 | }: React.HTMLAttributes) => (
74 |
81 | );
82 | DialogFooter.displayName = "DialogFooter";
83 |
84 | const DialogTitle = React.forwardRef<
85 | React.ElementRef,
86 | React.ComponentPropsWithoutRef
87 | >(({ className, ...props }, ref) => (
88 |
96 | ));
97 | DialogTitle.displayName = DialogPrimitive.Title.displayName;
98 |
99 | const DialogDescription = React.forwardRef<
100 | React.ElementRef,
101 | React.ComponentPropsWithoutRef
102 | >(({ className, ...props }, ref) => (
103 |
108 | ));
109 | DialogDescription.displayName = DialogPrimitive.Description.displayName;
110 |
111 | export {
112 | Dialog,
113 | DialogPortal,
114 | DialogOverlay,
115 | DialogClose,
116 | DialogTrigger,
117 | DialogContent,
118 | DialogHeader,
119 | DialogFooter,
120 | DialogTitle,
121 | DialogDescription,
122 | };
123 |
--------------------------------------------------------------------------------
/components/ui/drawer.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as React from "react";
4 | import { Drawer as DrawerPrimitive } from "vaul";
5 |
6 | import { cn } from "@/lib/utils";
7 |
8 | const Drawer = ({
9 | shouldScaleBackground = true,
10 | ...props
11 | }: React.ComponentProps) => (
12 |
16 | );
17 | Drawer.displayName = "Drawer";
18 |
19 | const DrawerTrigger = DrawerPrimitive.Trigger;
20 |
21 | const DrawerPortal = DrawerPrimitive.Portal;
22 |
23 | const DrawerClose = DrawerPrimitive.Close;
24 |
25 | const DrawerOverlay = React.forwardRef<
26 | React.ElementRef,
27 | React.ComponentPropsWithoutRef
28 | >(({ className, ...props }, ref) => (
29 |
34 | ));
35 | DrawerOverlay.displayName = DrawerPrimitive.Overlay.displayName;
36 |
37 | const DrawerContent = React.forwardRef<
38 | React.ElementRef,
39 | React.ComponentPropsWithoutRef
40 | >(({ className, children, ...props }, ref) => (
41 |
42 |
43 |
51 |
52 | {children}
53 |
54 |
55 | ));
56 | DrawerContent.displayName = "DrawerContent";
57 |
58 | const DrawerHeader = ({
59 | className,
60 | ...props
61 | }: React.HTMLAttributes) => (
62 |
66 | );
67 | DrawerHeader.displayName = "DrawerHeader";
68 |
69 | const DrawerFooter = ({
70 | className,
71 | ...props
72 | }: React.HTMLAttributes) => (
73 |
77 | );
78 | DrawerFooter.displayName = "DrawerFooter";
79 |
80 | const DrawerTitle = React.forwardRef<
81 | React.ElementRef,
82 | React.ComponentPropsWithoutRef
83 | >(({ className, ...props }, ref) => (
84 |
92 | ));
93 | DrawerTitle.displayName = DrawerPrimitive.Title.displayName;
94 |
95 | const DrawerDescription = React.forwardRef<
96 | React.ElementRef,
97 | React.ComponentPropsWithoutRef
98 | >(({ className, ...props }, ref) => (
99 |
104 | ));
105 | DrawerDescription.displayName = DrawerPrimitive.Description.displayName;
106 |
107 | export {
108 | Drawer,
109 | DrawerPortal,
110 | DrawerOverlay,
111 | DrawerTrigger,
112 | DrawerClose,
113 | DrawerContent,
114 | DrawerHeader,
115 | DrawerFooter,
116 | DrawerTitle,
117 | DrawerDescription,
118 | };
119 |
--------------------------------------------------------------------------------
/components/ui/input.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 |
3 | import { cn } from "@/lib/utils";
4 |
5 | export interface InputProps
6 | extends React.InputHTMLAttributes {}
7 |
8 | const Input = React.forwardRef(
9 | ({ className, type, ...props }, ref) => {
10 | return (
11 |
20 | );
21 | },
22 | );
23 | Input.displayName = "Input";
24 |
25 | export { Input };
26 |
--------------------------------------------------------------------------------
/components/ui/label.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as React from "react";
4 | import * as LabelPrimitive from "@radix-ui/react-label";
5 | import { cva, type VariantProps } from "class-variance-authority";
6 |
7 | import { cn } from "@/lib/utils";
8 |
9 | const labelVariants = cva(
10 | "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70",
11 | );
12 |
13 | const Label = React.forwardRef<
14 | React.ElementRef,
15 | React.ComponentPropsWithoutRef &
16 | VariantProps
17 | >(({ className, ...props }, ref) => (
18 |
23 | ));
24 | Label.displayName = LabelPrimitive.Root.displayName;
25 |
26 | export { Label };
27 |
--------------------------------------------------------------------------------
/components/ui/popover.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as React from "react";
4 | import * as PopoverPrimitive from "@radix-ui/react-popover";
5 |
6 | import { cn } from "@/lib/utils";
7 |
8 | const Popover = PopoverPrimitive.Root;
9 |
10 | const PopoverTrigger = PopoverPrimitive.Trigger;
11 |
12 | const PopoverContent = React.forwardRef<
13 | React.ElementRef,
14 | React.ComponentPropsWithoutRef
15 | >(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
16 |
17 |
27 |
28 | ));
29 | PopoverContent.displayName = PopoverPrimitive.Content.displayName;
30 |
31 | export { Popover, PopoverTrigger, PopoverContent };
32 |
--------------------------------------------------------------------------------
/components/ui/scroll-area.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as React from "react";
4 | import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area";
5 |
6 | import { cn } from "@/lib/utils";
7 |
8 | const ScrollArea = React.forwardRef<
9 | React.ElementRef,
10 | React.ComponentPropsWithoutRef
11 | >(({ className, children, ...props }, ref) => (
12 |
17 |
18 | {children}
19 |
20 |
21 |
22 |
23 | ));
24 | ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName;
25 |
26 | const ScrollBar = React.forwardRef<
27 | React.ElementRef,
28 | React.ComponentPropsWithoutRef
29 | >(({ className, orientation = "vertical", ...props }, ref) => (
30 |
43 |
44 |
45 | ));
46 | ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName;
47 |
48 | export { ScrollArea, ScrollBar };
49 |
--------------------------------------------------------------------------------
/components/ui/sonner.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { useTheme } from "next-themes";
4 | import { Toaster as Sonner } from "sonner";
5 |
6 | type ToasterProps = React.ComponentProps;
7 |
8 | const Toaster = ({ ...props }: ToasterProps) => {
9 | const { theme = "system" } = useTheme();
10 |
11 | return (
12 |
28 | );
29 | };
30 |
31 | export { Toaster };
32 |
--------------------------------------------------------------------------------
/components/ui/tabs.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as React from "react";
4 | import * as TabsPrimitive from "@radix-ui/react-tabs";
5 |
6 | import { cn } from "@/lib/utils";
7 |
8 | const Tabs = TabsPrimitive.Root;
9 |
10 | const TabsList = React.forwardRef<
11 | React.ElementRef,
12 | React.ComponentPropsWithoutRef
13 | >(({ className, ...props }, ref) => (
14 |
22 | ));
23 | TabsList.displayName = TabsPrimitive.List.displayName;
24 |
25 | const TabsTrigger = React.forwardRef<
26 | React.ElementRef,
27 | React.ComponentPropsWithoutRef
28 | >(({ className, ...props }, ref) => (
29 |
37 | ));
38 | TabsTrigger.displayName = TabsPrimitive.Trigger.displayName;
39 |
40 | const TabsContent = React.forwardRef<
41 | React.ElementRef,
42 | React.ComponentPropsWithoutRef
43 | >(({ className, ...props }, ref) => (
44 |
52 | ));
53 | TabsContent.displayName = TabsPrimitive.Content.displayName;
54 |
55 | export { Tabs, TabsList, TabsTrigger, TabsContent };
56 |
--------------------------------------------------------------------------------
/components/ui/textarea.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 |
3 | import { cn } from "@/lib/utils";
4 |
5 | export interface TextareaProps
6 | extends React.TextareaHTMLAttributes {}
7 |
8 | const Textarea = React.forwardRef(
9 | ({ className, ...props }, ref) => {
10 | return (
11 |
19 | );
20 | },
21 | );
22 | Textarea.displayName = "Textarea";
23 |
24 | export { Textarea };
25 |
--------------------------------------------------------------------------------
/components/ui/tooltip.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as React from "react";
4 | import * as TooltipPrimitive from "@radix-ui/react-tooltip";
5 |
6 | import { cn } from "@/lib/utils";
7 |
8 | const TooltipProvider = TooltipPrimitive.Provider;
9 |
10 | const Tooltip = TooltipPrimitive.Root;
11 |
12 | const TooltipTrigger = TooltipPrimitive.Trigger;
13 |
14 | const TooltipContent = React.forwardRef<
15 | React.ElementRef,
16 | React.ComponentPropsWithoutRef
17 | >(({ className, sideOffset = 4, ...props }, ref) => (
18 |
27 | ));
28 | TooltipContent.displayName = TooltipPrimitive.Content.displayName;
29 |
30 | export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider };
31 |
--------------------------------------------------------------------------------
/docker-compose.yaml:
--------------------------------------------------------------------------------
1 | version: "3"
2 | services:
3 | nginx:
4 | build:
5 | context: "./nginx"
6 | dockerfile: Dockerfile
7 | ports:
8 | - 80:80
9 | container_name: nginx-container
10 | depends_on:
11 | - client
12 | - api
13 | restart: always
14 | # networks:
15 | # - google-docs-network
16 |
17 | client:
18 | build:
19 | context: "./client"
20 | dockerfile: Dockerfile
21 | # ports:
22 | # - 4173:4173
23 | stdin_open: true
24 | container_name: client-container
25 | # networks:
26 | # - google-docs-network
27 | volumes:
28 | - ./client:/app
29 | depends_on:
30 | - api
31 |
32 | api:
33 | build:
34 | context: "./server"
35 | dockerfile: Dockerfile
36 | restart: always
37 | # ports:
38 | # - 4000:4000
39 | container_name: server-container
40 | # networks:
41 | # - google-docs-network
42 | volumes:
43 | - ./server:/app
44 | env_file:
45 | - ./server/.env
46 | depends_on:
47 | - db
48 |
49 | db:
50 | image: mongo:latest
51 | restart: always
52 | # ports:
53 | # - 27017:27017
54 | container_name: mongodb-container
55 | volumes:
56 | - dbData:/data/db
57 | env_file:
58 | - ./db/.env
59 |
60 | volumes:
61 | dbData: {}
62 |
--------------------------------------------------------------------------------
/eslint.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | parser: "@babel/eslint-parser",
3 | extends: ["eslint:recommended", "plugin:react/recommended"],
4 | plugins: ["react", "react-hooks"],
5 | rules: {
6 | "no-unused-vars": "error",
7 | "react-hooks/exhaustive-deps": "error",
8 | semi: ["error", "always"],
9 | },
10 | };
11 |
--------------------------------------------------------------------------------
/helpers/generateJWT.ts:
--------------------------------------------------------------------------------
1 | import { JWTPayload, SignJWT, importJWK } from "jose";
2 |
3 | export const generateJWT = async (payload: JWTPayload) => {
4 | const secret = process.env.JWT_SECRET || "secret";
5 |
6 | const jwk = await importJWK({ k: secret, alg: "HS256", kty: "oct" });
7 |
8 | const jwt = await new SignJWT(payload)
9 | .setProtectedHeader({ alg: "HS256" })
10 | .setIssuedAt()
11 | .setExpirationTime("365d")
12 | .sign(jwk);
13 |
14 | return jwt;
15 | };
16 |
--------------------------------------------------------------------------------
/helpers/getInitials.ts:
--------------------------------------------------------------------------------
1 | const getInitials = (name: string) => {
2 | let initials = name.split(" ");
3 |
4 | if (initials.length > 2) return initials[0][0] + initials[1][0];
5 | return initials[0][0];
6 | };
7 |
8 | export default getInitials;
9 |
--------------------------------------------------------------------------------
/helpers/getRandomColor.ts:
--------------------------------------------------------------------------------
1 | const colors = [
2 | "#958DF1",
3 | "#F98181",
4 | "#FBBC88",
5 | "#FAF594",
6 | "#70CFF8",
7 | "#94FADB",
8 | "#B9F18D",
9 | "#C3E2C2",
10 | "#EAECCC",
11 | "#AFC8AD",
12 | "#EEC759",
13 | "#9BB8CD",
14 | "#FF90BC",
15 | "#FFC0D9",
16 | "#DC8686",
17 | "#7ED7C1",
18 | "#F3EEEA",
19 | "#89B9AD",
20 | "#D0BFFF",
21 | "#FFF8C9",
22 | "#CBFFA9",
23 | "#9BABB8",
24 | "#E3F4F4",
25 | ];
26 |
27 | export const getRandomColor = () => {
28 | return colors[Math.floor(Math.random() * colors.length)];
29 | };
30 |
--------------------------------------------------------------------------------
/helpers/prettifyDates.ts:
--------------------------------------------------------------------------------
1 | type OptionsType = {
2 | weekday?: "narrow" | "short" | "long";
3 | year?: "numeric" | "2-digit";
4 | month?: "numeric" | "2-digit" | "narrow" | "short" | "long";
5 | day?: "numeric" | "2-digit";
6 | hour?: "numeric" | "2-digit";
7 | minute?: "numeric" | "2-digit";
8 | second?: "numeric" | "2-digit";
9 | timeZoneName?: "short" | "long";
10 | hour12?: boolean;
11 | era?: "narrow" | "short" | "long";
12 | timeZone?: string;
13 | fractionalSecondDigits?: 1 | 2 | 3;
14 | };
15 |
16 | const prettifyDate = (dateString: string | undefined, options: OptionsType) => {
17 | if (!dateString) return "";
18 | const date = new Date(dateString);
19 |
20 | return new Intl.DateTimeFormat("en-US", options).format(date);
21 | };
22 |
23 | export default prettifyDate;
24 |
--------------------------------------------------------------------------------
/lib/auth.ts:
--------------------------------------------------------------------------------
1 | import { NextAuthOptions } from "next-auth";
2 | import GoogleProvider from "next-auth/providers/google";
3 |
4 | import prisma from "@/prisma/prismaClient";
5 |
6 | export const authOptions: NextAuthOptions = {
7 | providers: [
8 | GoogleProvider({
9 | clientId: process.env.GOOGLE_ID as string,
10 | clientSecret: process.env.GOOGLE_SECRET as string,
11 |
12 | authorization: {
13 | params: {
14 | prompt: "consent",
15 | access_type: "offline",
16 | response_type: "code",
17 | },
18 | },
19 | }),
20 | ],
21 | secret: process.env.NEXTAUTH_SECRET,
22 | callbacks: {
23 | async signIn({ account, profile }: any) {
24 | if (account?.provider === "google") {
25 | const {
26 | email,
27 | email_verified,
28 | picture,
29 | name,
30 | given_name,
31 | family_name,
32 | } = profile;
33 |
34 | if (!email_verified) return false;
35 |
36 | try {
37 | const username = given_name + family_name;
38 |
39 | await prisma.user.upsert({
40 | where: { email },
41 | update: {
42 | isVerified: true,
43 | },
44 | create: {
45 | name: name,
46 | username: username,
47 | email: email,
48 | password: null,
49 | picture,
50 | isVerified: true,
51 | },
52 | });
53 |
54 | return true;
55 | } catch (e) {
56 | console.log(e);
57 | return false;
58 | }
59 | }
60 | return true;
61 | },
62 | session: async ({ session }: any) => {
63 | if (session.user) {
64 | const user = await prisma.user.findFirst({
65 | where: { email: session.user.email },
66 | });
67 | session.user.id = user?.id;
68 | }
69 | return session;
70 | },
71 | redirect({ baseUrl }) {
72 | return `${baseUrl}/document`;
73 | },
74 | },
75 | cookies: {
76 | sessionToken: {
77 | name: "next-auth.session-token",
78 | options: {
79 | httpOnly: true,
80 | sameSite: "lax",
81 | path: "/",
82 | secure: true,
83 | },
84 | },
85 | },
86 | pages: {
87 | signIn: "/signin",
88 | },
89 | } satisfies NextAuthOptions;
90 |
--------------------------------------------------------------------------------
/lib/customHooks/ReturnType.ts:
--------------------------------------------------------------------------------
1 | export type SessionReturnType = {
2 | id: string | null | undefined;
3 | name: string | null | undefined;
4 | email: string | null | undefined;
5 | image: string | null | undefined;
6 | };
7 |
--------------------------------------------------------------------------------
/lib/customHooks/action.ts:
--------------------------------------------------------------------------------
1 | "use server";
2 |
3 | import { jwtDecode } from "jwt-decode";
4 | import { cookies } from "next/headers";
5 |
6 | export async function GetUserDetails() {
7 | const token = cookies().get("token")?.value;
8 |
9 | if (!token) return;
10 | const decoded = jwtDecode(token!);
11 | return decoded;
12 | }
13 |
--------------------------------------------------------------------------------
/lib/customHooks/getServerSession.ts:
--------------------------------------------------------------------------------
1 | // @ts-nocheck
2 | import { getServerSession as nextAuthSession } from "next-auth";
3 |
4 | import { ReturnType } from "./ReturnType";
5 | import { GetUserDetails } from "./action";
6 | import { authOptions } from "../auth";
7 |
8 | export default async function getServerSession(): Promise {
9 | const session = await nextAuthSession(authOptions);
10 |
11 | if (session)
12 | return {
13 | id: session.user?.id,
14 | name: session.user?.name,
15 | email: session.user?.email,
16 | image: session.user?.image,
17 | };
18 |
19 | const userDetails = await GetUserDetails();
20 | return {
21 | id: userDetails?.id,
22 | name: userDetails?.name,
23 | email: userDetails?.email,
24 | image: userDetails?.picture,
25 | };
26 | }
27 |
--------------------------------------------------------------------------------
/lib/customHooks/useClientSession.tsx:
--------------------------------------------------------------------------------
1 | // @ts-nocheck
2 | "use client";
3 |
4 | import { useEffect, useState } from "react";
5 | import { useSession } from "next-auth/react";
6 |
7 | import { GetUserDetails } from "./action";
8 | import { ReturnType } from "./ReturnType";
9 |
10 | export default function useClientSession(): ReturnType {
11 | const [user, setUser] = useState(null);
12 |
13 | const { data: session } = useSession();
14 |
15 | useEffect(() => {
16 | (async () => {
17 | if (session === null) {
18 | const userDetails = await GetUserDetails();
19 | setUser({
20 | id: userDetails?.id,
21 | name: userDetails?.name,
22 | email: userDetails?.email,
23 | image: userDetails?.picture,
24 | });
25 | }
26 | })();
27 | }, [session]);
28 |
29 | if (session)
30 | return {
31 | id: session.user?.id,
32 | name: session.user?.name,
33 | email: session.user?.email,
34 | image: session.user?.image,
35 | };
36 |
37 | return {
38 | id: user?.id,
39 | name: user?.name,
40 | email: user?.email,
41 | image: user?.image,
42 | };
43 | }
44 |
--------------------------------------------------------------------------------
/lib/customHooks/useDebounce.tsx:
--------------------------------------------------------------------------------
1 | import { debounce } from "lodash";
2 | import { useEffect, useMemo, useRef } from "react";
3 |
4 | export default function useDebounce(
5 | callbackFunction: ((value: D) => void) | ((value: D) => Promise),
6 | debounceDelay: number,
7 | ) {
8 | const callbackRef = useRef(callbackFunction);
9 |
10 | useEffect(() => {
11 | callbackRef.current = callbackFunction;
12 | }, [callbackFunction]);
13 |
14 | const debounceFunc = useMemo(
15 | () => debounce((value: D) => callbackRef.current(value), debounceDelay),
16 | [debounceDelay],
17 | );
18 |
19 | return debounceFunc;
20 | }
21 |
--------------------------------------------------------------------------------
/lib/customHooks/useMotionTimeline.tsx:
--------------------------------------------------------------------------------
1 | // Source code: https://gist.github.com/TomIsLoading/ae9c5af702f242df461c1cb1be5c33a4
2 |
3 | import { useEffect, useRef } from "react";
4 | import {
5 | ElementOrSelector,
6 | DOMKeyframesDefinition,
7 | DynamicAnimationOptions,
8 | useAnimate,
9 | } from "framer-motion";
10 |
11 | type AnimateParams = [
12 | ElementOrSelector,
13 | DOMKeyframesDefinition,
14 | DynamicAnimationOptions,
15 | ];
16 |
17 | type Animation = AnimateParams | Animation[];
18 |
19 | export default function useMotionTimeline(
20 | keyframes: Animation[],
21 | count: number = 1,
22 | isHovered: boolean,
23 | ) {
24 | const mounted = useRef(true);
25 |
26 | const [scope, animate] = useAnimate();
27 |
28 | useEffect(() => {
29 | mounted.current = true;
30 |
31 | if (isHovered) {
32 | handleAnimate();
33 | }
34 |
35 | return () => {
36 | mounted.current = false;
37 | };
38 | }, [isHovered]);
39 |
40 | const processAnimation = async (animation: Animation) => {
41 | // If list of animations, run all concurrently
42 | if (Array.isArray(animation[0])) {
43 | await Promise.all(
44 | animation.map(async (a) => {
45 | await processAnimation(a as Animation);
46 | })
47 | );
48 | } else {
49 | // Else run the single animation
50 | await animate(...(animation as AnimateParams));
51 | }
52 | };
53 |
54 | const handleAnimate = async () => {
55 | for (let i = 0; i < count; i++) {
56 | for (const animation of keyframes) {
57 | if (!mounted.current) return;
58 | await processAnimation(animation);
59 | }
60 | }
61 | };
62 |
63 | return scope;
64 | };
65 |
--------------------------------------------------------------------------------
/lib/guestServices.ts:
--------------------------------------------------------------------------------
1 | import { uuidv4 as uuid } from "lib0/random.js";
2 |
3 | import Avatar from "@/public/profilepic_placeholder.png"
4 | import type { Document, User } from ".prisma/client";
5 |
6 | export const createGuestUser = () => {
7 | const user = {
8 | id: uuid(),
9 | username: "anonymous",
10 | name: "Anonymous",
11 | email: "anonymous@email.com",
12 | password: null,
13 | picture: Avatar.src,
14 | isVerified: false,
15 | verifyCode: null,
16 | verifyCodeExpiry: null,
17 | joinedAt: new Date(),
18 | }
19 | localStorage.setItem('user', JSON.stringify(user))
20 |
21 | return user;
22 | }
23 | export const getGuestUser = () => {
24 | let user: User = JSON.parse(localStorage.getItem('user') || '{}');
25 | if (!user?.id) {
26 | user = createGuestUser();
27 | }
28 |
29 | return user;
30 | }
31 | export const createGuestDocument = () => {
32 | const user = getGuestUser();
33 |
34 | const newDocument: Document = {
35 | id: uuid(),
36 | userId: user.id,
37 | name: "Untitled document",
38 | data: "",
39 | createdAt: new Date(),
40 | updatedAt: new Date(),
41 | thumbnail: null,
42 | deleteUrl: null
43 | }
44 |
45 | const allDocuments: Document[] = JSON.parse(localStorage.getItem('documents') || '[]');
46 | localStorage.setItem('documents', JSON.stringify([...allDocuments, newDocument]))
47 |
48 | return newDocument;
49 | }
50 | export const getAllGuestDocuments = () => {
51 | const documents: Document[] = JSON.parse(localStorage.getItem('documents') || '[]') as Document[];
52 |
53 | const user = getGuestUser();
54 |
55 | const data = documents.map((doc) => {
56 | return {
57 | id: doc.id,
58 | thumbnail: doc.thumbnail,
59 | name: doc.name,
60 | updatedAt: doc.updatedAt,
61 | createdBy: user.id,
62 | users: [{
63 | user: {
64 | name: user.name,
65 | picture: user.picture
66 | }
67 | }]
68 | }
69 | })
70 | return data.reverse();
71 | }
72 | export const getGuestDocumentDetails = (docId: string) => {
73 | const allDocuments: Document[] = JSON.parse(localStorage.getItem('documents') || '[]');
74 |
75 | let document = allDocuments.find(e => e.id === docId);
76 | if (!document) return;
77 |
78 | return document;
79 | }
80 | export const updateGuestDocument = (docId: string, docProp: string, updateValue: string) => {
81 | const document = getGuestDocumentDetails(docId);
82 | if (!document) return;
83 |
84 | const allDocuments: Document[] = JSON.parse(localStorage.getItem('documents') || '[]');
85 | const index = allDocuments.findIndex((e) => e.id === document.id);
86 |
87 | if (docProp === 'thumbnail') {
88 | allDocuments.splice(index, 1, { ...document, [docProp]: `data:image/png;base64,${updateValue}` });
89 | } else {
90 | allDocuments.splice(index, 1, { ...document, [docProp]: updateValue });
91 | }
92 | localStorage.setItem('documents', JSON.stringify(allDocuments));
93 | }
94 | export const deleteGuestDocument = (docId: string) => {
95 | const document = getGuestDocumentDetails(docId);
96 | if (!document) return;
97 |
98 | const allDocuments: Document[] = JSON.parse(localStorage.getItem('documents') || '[]');
99 | const index = allDocuments.findIndex((e) => e.id === document.id);
100 |
101 | allDocuments.splice(index, 1);
102 | localStorage.setItem('documents', JSON.stringify(allDocuments));
103 | }
104 |
--------------------------------------------------------------------------------
/lib/mail/EmailVerificationMailTemplate.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | Body,
3 | Container,
4 | Head,
5 | Heading,
6 | Html,
7 | Img,
8 | Preview,
9 | Section,
10 | Text,
11 | } from "@react-email/components";
12 | import * as React from "react";
13 |
14 | interface EmailVerificationMailTemplatePropType {
15 | verifyCode?: string;
16 | }
17 |
18 | export const EmailVerificationMailTemplate = ({
19 | verifyCode,
20 | }: EmailVerificationMailTemplatePropType) => (
21 |
22 |
23 | Confirm your email address
24 |
25 |
26 |
27 |
33 |
34 | Confirm your email address
35 |
36 | Your confirmation code is below - enter it in your open browser window
37 | and we'll help you get signed in.
38 |
39 |
40 |
43 |
44 |
45 | If you didn't request this email, there's nothing to worry
46 | about, you can safely ignore it.
47 |
48 |
49 |
50 |
51 | );
52 |
53 | EmailVerificationMailTemplate.PreviewProps = {
54 | validationCode: "DJZ-TLX",
55 | } as EmailVerificationMailTemplatePropType;
56 |
57 | export default EmailVerificationMailTemplate;
58 |
59 | const main = {
60 | backgroundColor: "#ffffff",
61 | margin: "0 auto",
62 | fontFamily:
63 | "-apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif",
64 | };
65 |
66 | const container = {
67 | margin: "0 auto",
68 | padding: "0px 20px",
69 | };
70 |
71 | const logoContainer = {
72 | marginTop: "32px",
73 | };
74 |
75 | const h1 = {
76 | color: "#1d1c1d",
77 | fontSize: "36px",
78 | fontWeight: "700",
79 | margin: "30px 0",
80 | padding: "0",
81 | lineHeight: "42px",
82 | };
83 |
84 | const heroText = {
85 | fontSize: "20px",
86 | lineHeight: "28px",
87 | marginBottom: "30px",
88 | };
89 |
90 | const codeBox = {
91 | background: "rgb(245, 244, 245)",
92 | borderRadius: "4px",
93 | marginBottom: "30px",
94 | padding: "40px 10px",
95 | };
96 |
97 | const confirmationCodeText = {
98 | fontSize: "30px",
99 | textAlign: "center" as const,
100 | verticalAlign: "middle",
101 | };
102 |
103 | const text = {
104 | color: "#000",
105 | fontSize: "14px",
106 | lineHeight: "24px",
107 | };
108 |
--------------------------------------------------------------------------------
/lib/mail/resetPasswordMailTemplate.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | Body,
3 | Button,
4 | Container,
5 | Head,
6 | Heading,
7 | Html,
8 | Img,
9 | Preview,
10 | Section,
11 | Text,
12 | } from "@react-email/components";
13 | import * as React from "react";
14 |
15 | interface PasswordResetMailTemplatePropType {
16 | userFirstname?: string;
17 | resetPasswordLink?: string;
18 | }
19 |
20 | export const PasswordResetMailTemplate = ({
21 | resetPasswordLink,
22 | }: PasswordResetMailTemplatePropType) => (
23 |
24 |
25 | Reset your Password.
26 |
27 |
28 |
29 |
35 |
36 |
37 | Reset your Password.
38 | Hi,
39 |
40 | Someone recently requested a password change for your DocX account. If
41 | this was you, you can set a new password here:
42 |
43 |
44 | Reset password
45 |
46 |
47 | If you didn't request this email, there's nothing to worry
48 | about, you can safely ignore it.
49 |
50 |
51 |
52 |
53 | );
54 |
55 | PasswordResetMailTemplate.PreviewProps = {
56 | validationCode: "DJZ-TLX",
57 | } as PasswordResetMailTemplatePropType;
58 |
59 | export default PasswordResetMailTemplate;
60 |
61 | const main = {
62 | backgroundColor: "#ffffff",
63 | margin: "0 auto",
64 | fontFamily:
65 | "-apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif",
66 | };
67 |
68 | const container = {
69 | margin: "0 auto",
70 | padding: "0px 20px",
71 | };
72 |
73 | const logoContainer = {
74 | display: "flex",
75 | marginTop: "32px",
76 | };
77 |
78 | const h1 = {
79 | color: "#1d1c1d",
80 | fontSize: "36px",
81 | fontWeight: "700",
82 | margin: "30px 0",
83 | padding: "0",
84 | lineHeight: "42px",
85 | };
86 |
87 | const text = {
88 | color: "#000",
89 | fontSize: "14px",
90 | lineHeight: "24px",
91 | };
92 |
93 | const button = {
94 | backgroundColor: "#007ee6",
95 | borderRadius: "4px",
96 | color: "#fff",
97 | fontFamily: "'Open Sans', 'Helvetica Neue', Arial",
98 | fontSize: "15px",
99 | textDecoration: "none",
100 | textAlign: "center" as const,
101 | display: "block",
102 | width: "210px",
103 | padding: "14px 7px",
104 | };
105 |
--------------------------------------------------------------------------------
/lib/mail/sendMail.ts:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Resend } from "resend";
3 |
4 | export const SendMail = async (
5 | to: string,
6 | subject: string,
7 | Template: React.ReactNode,
8 | ) => {
9 | const resend = new Resend(process.env.RESEND_API_KEY);
10 | try {
11 | const { data, error } = await resend.emails.send({
12 | from: "docx@pbcreates.xyz",
13 | to,
14 | subject,
15 | react: Template,
16 | });
17 |
18 | if (error)
19 | return {
20 | success: false,
21 | error: error.message,
22 | };
23 |
24 | return { success: true, data };
25 | } catch (e) {
26 | console.log(e);
27 | return { success: false, error: "Internal server error" };
28 | }
29 | };
30 |
--------------------------------------------------------------------------------
/lib/utils.ts:
--------------------------------------------------------------------------------
1 | import { type ClassValue, clsx } from "clsx";
2 | import { twMerge } from "tailwind-merge";
3 |
4 | export function cn(...inputs: ClassValue[]) {
5 | return twMerge(clsx(inputs));
6 | }
7 |
--------------------------------------------------------------------------------
/middleware.ts:
--------------------------------------------------------------------------------
1 | import { NextResponse } from "next/server";
2 | import type { NextRequest } from "next/server";
3 |
4 | export async function middleware(request: NextRequest) {
5 | const token = request.cookies.get("token")?.value;
6 | const nextAuthSession = request.cookies.get("next-auth.session-token")?.value;
7 |
8 | const isAuthorized = token || nextAuthSession;
9 |
10 | if (request.url.split("/")[3] === "") {
11 | if (isAuthorized) {
12 | return NextResponse.redirect(new URL("/document", request.url));
13 | }
14 | return NextResponse.next();
15 | } else {
16 | // if (!isAuthorized) {
17 | // return NextResponse.redirect(new URL("/api/auth/signin", request.url));
18 | // }
19 | return NextResponse.next();
20 | }
21 | }
22 |
23 | export const config = {
24 | matcher: ["/", "/document", "/writer/:id*"],
25 | };
26 |
--------------------------------------------------------------------------------
/next-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | ///
3 |
4 | // NOTE: This file should not be edited
5 | // see https://nextjs.org/docs/basic-features/typescript for more information.
6 |
--------------------------------------------------------------------------------
/next.config.mjs:
--------------------------------------------------------------------------------
1 | /** @type {import('next').NextConfig} */
2 | const nextConfig = {};
3 |
4 | export default nextConfig;
5 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "next-template",
3 | "version": "0.1.0",
4 | "private": true,
5 | "scripts": {
6 | "dev": "next dev",
7 | "build": "next build",
8 | "start": "next start",
9 | "lint": "next lint",
10 | "lint:check": "eslint .",
11 | "format": "prettier --write .",
12 | "format:check": "prettier --check .",
13 | "db:migrate": "npx prisma migrate",
14 | "db:generate": "npx prisma generate",
15 | "db:run": "yarn run db:migrate && yarn run db:generate",
16 | "install:prod": "yarn && yarn run db:run"
17 | },
18 | "dependencies": {
19 | "@auth0/nextjs-auth0": "^3.5.0",
20 | "@google/generative-ai": "^0.21.0",
21 | "@hocuspocus/provider": "^2.13.5",
22 | "@lexical/react": "^0.22.0",
23 | "@lexical/yjs": "^0.22.0",
24 | "@prisma/client": "^5.16.2",
25 | "@radix-ui/react-avatar": "^1.1.0",
26 | "@radix-ui/react-dialog": "^1.1.1",
27 | "@radix-ui/react-dropdown-menu": "^2.1.2",
28 | "@radix-ui/react-label": "^2.1.0",
29 | "@radix-ui/react-popover": "^1.1.1",
30 | "@radix-ui/react-scroll-area": "^1.1.0",
31 | "@radix-ui/react-select": "^2.1.1",
32 | "@radix-ui/react-slot": "^1.1.0",
33 | "@radix-ui/react-tabs": "^1.1.0",
34 | "@radix-ui/react-tooltip": "^1.1.2",
35 | "@react-email/components": "^0.0.25",
36 | "@tanstack/react-query": "^5.50.1",
37 | "@tanstack/react-query-devtools": "^5.50.1",
38 | "@tiptap/extension-collaboration": "^2.6.2",
39 | "@tiptap/extension-collaboration-cursor": "^2.6.2",
40 | "@tiptap/extension-color": "^2.4.0",
41 | "@tiptap/extension-document": "^2.4.0",
42 | "@tiptap/extension-font-family": "^2.4.0",
43 | "@tiptap/extension-highlight": "^2.4.0",
44 | "@tiptap/extension-list-item": "^2.4.0",
45 | "@tiptap/extension-text-align": "^2.4.0",
46 | "@tiptap/extension-text-style": "^2.4.0",
47 | "@tiptap/extension-underline": "^2.4.0",
48 | "@tiptap/html": "^2.5.8",
49 | "@tiptap/pm": "^2.4.0",
50 | "@tiptap/react": "^2.4.0",
51 | "@tiptap/starter-kit": "^2.4.0",
52 | "@types/jsonwebtoken": "^9.0.6",
53 | "@vercel/analytics": "^1.3.1",
54 | "axios": "^1.7.2",
55 | "bcryptjs": "^2.4.3",
56 | "class-variance-authority": "^0.7.0",
57 | "clsx": "^2.1.1",
58 | "framer-motion": "^11.5.0",
59 | "html2canvas": "^1.4.1",
60 | "input-otp": "^1.2.4",
61 | "jose": "^5.6.3",
62 | "jsonwebtoken": "^9.0.2",
63 | "jwt-decode": "^4.0.0",
64 | "lenis": "^1.1.18",
65 | "lexical": "^0.22.0",
66 | "lib0": "^0.2.97",
67 | "lodash": "^4.17.21",
68 | "lodash.debounce": "^4.0.8",
69 | "lucide-react": "^0.400.0",
70 | "next": "14.2.4",
71 | "next-auth": "^4.24.7",
72 | "next-themes": "^0.3.0",
73 | "otp-generator": "^4.0.1",
74 | "prettier": "^3.3.3",
75 | "react": "^18.3.1",
76 | "react-colorful": "^5.6.1",
77 | "react-dom": "^18.3.1",
78 | "react-hook-form": "^7.52.1",
79 | "resend": "^4.0.0",
80 | "sharp": "^0.33.4",
81 | "sonner": "^1.5.0",
82 | "tailwind-merge": "^2.5.2",
83 | "tailwindcss-animate": "^1.0.7",
84 | "vaul": "^0.9.1",
85 | "ws": "^8.18.0",
86 | "y-prosemirror": "^1.2.12",
87 | "y-protocols": "^1.0.6",
88 | "y-webrtc": "^10.3.0",
89 | "y-websocket": "^2.1.0",
90 | "yjs": "^13.6.21",
91 | "zod": "^3.23.8"
92 | },
93 | "devDependencies": {
94 | "@tailwindcss/typography": "^0.5.13",
95 | "@tanstack/eslint-plugin-query": "^5.50.1",
96 | "@types/lodash": "^4.17.6",
97 | "@types/node": "^20",
98 | "@types/react": "^18",
99 | "@types/react-dom": "^18",
100 | "eslint": "^8.57.0",
101 | "eslint-config-next": "14.2.4",
102 | "eslint-plugin-react": "^7.35.2",
103 | "eslint-plugin-react-hooks": "^4.6.2",
104 | "postcss": "^8",
105 | "prisma": "^5.16.2",
106 | "tailwindcss": "^3.4.1",
107 | "typescript": "^5"
108 | }
109 | }
110 |
--------------------------------------------------------------------------------
/postcss.config.mjs:
--------------------------------------------------------------------------------
1 | /** @type {import('postcss-load-config').Config} */
2 | const config = {
3 | plugins: {
4 | tailwindcss: {},
5 | },
6 | };
7 |
8 | export default config;
9 |
--------------------------------------------------------------------------------
/prisma/migrations/20240709131843_init/migration.sql:
--------------------------------------------------------------------------------
1 | -- CreateTable
2 | CREATE TABLE "User" (
3 | "id" TEXT NOT NULL,
4 | "username" TEXT NOT NULL,
5 | "email" TEXT NOT NULL,
6 | "name" TEXT NOT NULL,
7 | "password" TEXT NOT NULL,
8 | "isVerified" BOOLEAN NOT NULL DEFAULT false,
9 | "joinedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
10 |
11 | CONSTRAINT "User_pkey" PRIMARY KEY ("id")
12 | );
13 |
14 | -- CreateTable
15 | CREATE TABLE "Document" (
16 | "id" TEXT NOT NULL,
17 | "name" TEXT NOT NULL DEFAULT 'Untitled Document',
18 | "data" TEXT NOT NULL,
19 |
20 | CONSTRAINT "Document_pkey" PRIMARY KEY ("id")
21 | );
22 |
23 | -- CreateTable
24 | CREATE TABLE "_DocumentToUser" (
25 | "A" TEXT NOT NULL,
26 | "B" TEXT NOT NULL
27 | );
28 |
29 | -- CreateIndex
30 | CREATE UNIQUE INDEX "User_email_key" ON "User"("email");
31 |
32 | -- CreateIndex
33 | CREATE UNIQUE INDEX "_DocumentToUser_AB_unique" ON "_DocumentToUser"("A", "B");
34 |
35 | -- CreateIndex
36 | CREATE INDEX "_DocumentToUser_B_index" ON "_DocumentToUser"("B");
37 |
38 | -- AddForeignKey
39 | ALTER TABLE "_DocumentToUser" ADD CONSTRAINT "_DocumentToUser_A_fkey" FOREIGN KEY ("A") REFERENCES "Document"("id") ON DELETE CASCADE ON UPDATE CASCADE;
40 |
41 | -- AddForeignKey
42 | ALTER TABLE "_DocumentToUser" ADD CONSTRAINT "_DocumentToUser_B_fkey" FOREIGN KEY ("B") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
43 |
--------------------------------------------------------------------------------
/prisma/migrations/20240715060410_15_07_2024/migration.sql:
--------------------------------------------------------------------------------
1 | /*
2 | Warnings:
3 |
4 | - You are about to drop the `_DocumentToUser` table. If the table is not empty, all the data it contains will be lost.
5 | - Added the required column `updatedAt` to the `Document` table without a default value. This is not possible if the table is not empty.
6 | - Added the required column `userId` to the `Document` table without a default value. This is not possible if the table is not empty.
7 | - Added the required column `verifyToken` to the `User` table without a default value. This is not possible if the table is not empty.
8 |
9 | */
10 | -- DropForeignKey
11 | ALTER TABLE "_DocumentToUser" DROP CONSTRAINT "_DocumentToUser_A_fkey";
12 |
13 | -- DropForeignKey
14 | ALTER TABLE "_DocumentToUser" DROP CONSTRAINT "_DocumentToUser_B_fkey";
15 |
16 | -- AlterTable
17 | ALTER TABLE "Document" ADD COLUMN "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
18 | ADD COLUMN "updatedAt" TIMESTAMP(3) NOT NULL,
19 | ADD COLUMN "userId" TEXT NOT NULL;
20 |
21 | -- AlterTable
22 | ALTER TABLE "User" ADD COLUMN "verifyToken" TEXT NOT NULL;
23 |
24 | -- DropTable
25 | DROP TABLE "_DocumentToUser";
26 |
27 | -- CreateTable
28 | CREATE TABLE "UserOnDocument" (
29 | "userId" TEXT NOT NULL,
30 | "documentId" TEXT NOT NULL,
31 | "assignedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
32 |
33 | CONSTRAINT "UserOnDocument_pkey" PRIMARY KEY ("userId","documentId")
34 | );
35 |
36 | -- AddForeignKey
37 | ALTER TABLE "Document" ADD CONSTRAINT "Document_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
38 |
39 | -- AddForeignKey
40 | ALTER TABLE "UserOnDocument" ADD CONSTRAINT "UserOnDocument_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
41 |
42 | -- AddForeignKey
43 | ALTER TABLE "UserOnDocument" ADD CONSTRAINT "UserOnDocument_documentId_fkey" FOREIGN KEY ("documentId") REFERENCES "Document"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
44 |
--------------------------------------------------------------------------------
/prisma/migrations/20240715112437_/migration.sql:
--------------------------------------------------------------------------------
1 | /*
2 | Warnings:
3 |
4 | - Changed the type of `data` on the `Document` table. No cast exists, the column would be dropped and recreated, which cannot be done if there is data, since the column is required.
5 |
6 | */
7 | -- AlterTable
8 | ALTER TABLE "Document" DROP COLUMN "data",
9 | ADD COLUMN "data" JSONB NOT NULL;
10 |
--------------------------------------------------------------------------------
/prisma/migrations/20240715114926_schema_change/migration.sql:
--------------------------------------------------------------------------------
1 | -- AlterTable
2 | ALTER TABLE "Document" ALTER COLUMN "data" DROP NOT NULL;
3 |
--------------------------------------------------------------------------------
/prisma/migrations/20240715134816_/migration.sql:
--------------------------------------------------------------------------------
1 | /*
2 | Warnings:
3 |
4 | - Made the column `data` on table `Document` required. This step will fail if there are existing NULL values in that column.
5 |
6 | */
7 | -- AlterTable
8 | ALTER TABLE "Document" ALTER COLUMN "data" SET NOT NULL,
9 | ALTER COLUMN "data" SET DATA TYPE TEXT;
10 |
--------------------------------------------------------------------------------
/prisma/migrations/20240716082106_cascade_delete/migration.sql:
--------------------------------------------------------------------------------
1 | -- DropForeignKey
2 | ALTER TABLE "UserOnDocument" DROP CONSTRAINT "UserOnDocument_documentId_fkey";
3 |
4 | -- AddForeignKey
5 | ALTER TABLE "UserOnDocument" ADD CONSTRAINT "UserOnDocument_documentId_fkey" FOREIGN KEY ("documentId") REFERENCES "Document"("id") ON DELETE CASCADE ON UPDATE CASCADE;
6 |
--------------------------------------------------------------------------------
/prisma/migrations/20240717054532_thumbnail/migration.sql:
--------------------------------------------------------------------------------
1 | -- AlterTable
2 | ALTER TABLE "Document" ADD COLUMN "thumbnail" TEXT;
3 |
--------------------------------------------------------------------------------
/prisma/migrations/20240724135206_init2/migration.sql:
--------------------------------------------------------------------------------
1 | -- AlterTable
2 | ALTER TABLE "Document" ADD COLUMN "deleteUrl" TEXT;
3 |
--------------------------------------------------------------------------------
/prisma/migrations/20240804151601_new_changes/migration.sql:
--------------------------------------------------------------------------------
1 | /*
2 | Warnings:
3 |
4 | - You are about to drop the column `verifyToken` on the `User` table. All the data in the column will be lost.
5 | - Added the required column `picture` to the `User` table without a default value. This is not possible if the table is not empty.
6 |
7 | */
8 | -- AlterTable
9 | ALTER TABLE "Document" ALTER COLUMN "updatedAt" SET DEFAULT CURRENT_TIMESTAMP;
10 |
11 | -- AlterTable
12 | ALTER TABLE "User" DROP COLUMN "verifyToken",
13 | ADD COLUMN "picture" TEXT NOT NULL;
14 |
--------------------------------------------------------------------------------
/prisma/migrations/20240805092134_password_null/migration.sql:
--------------------------------------------------------------------------------
1 | -- AlterTable
2 | ALTER TABLE "User" ALTER COLUMN "password" DROP NOT NULL;
3 |
--------------------------------------------------------------------------------
/prisma/migrations/20240805092953_picture_null/migration.sql:
--------------------------------------------------------------------------------
1 | -- AlterTable
2 | ALTER TABLE "User" ALTER COLUMN "picture" DROP NOT NULL;
3 |
--------------------------------------------------------------------------------
/prisma/migrations/20241020044532_otp/migration.sql:
--------------------------------------------------------------------------------
1 | -- AlterTable
2 | ALTER TABLE "User" ADD COLUMN "verifyCode" TEXT,
3 | ADD COLUMN "verifyCodeExpiry" TIMESTAMP(3);
4 |
--------------------------------------------------------------------------------
/prisma/migrations/migration_lock.toml:
--------------------------------------------------------------------------------
1 | # Please do not edit this file manually
2 | # It should be added in your version-control system (i.e. Git)
3 | provider = "postgresql"
--------------------------------------------------------------------------------
/prisma/prismaClient.ts:
--------------------------------------------------------------------------------
1 | import { PrismaClient } from "@prisma/client";
2 |
3 | const prismaClientSingleton = () => {
4 | return new PrismaClient();
5 | };
6 |
7 | declare const globalThis: {
8 | prismaGlobal: ReturnType;
9 | } & typeof global;
10 |
11 | const prisma = globalThis.prismaGlobal ?? prismaClientSingleton();
12 |
13 | export default prisma;
14 |
15 | if (process.env.NODE_ENV !== "production") globalThis.prismaGlobal = prisma;
16 |
--------------------------------------------------------------------------------
/prisma/schema.prisma:
--------------------------------------------------------------------------------
1 | // This is your Prisma schema file,
2 | // learn more about it in the docs: https://pris.ly/d/prisma-schema
3 |
4 | // Looking for ways to speed up your queries, or scale easily with your serverless or edge functions?
5 | // Try Prisma Accelerate: https://pris.ly/cli/accelerate-init
6 |
7 | generator client {
8 | provider = "prisma-client-js"
9 | }
10 |
11 | datasource db {
12 | provider = "postgresql"
13 | url = env("DATABASE_URL")
14 | }
15 |
16 | model User {
17 | id String @id @default(uuid())
18 | username String
19 | email String @unique
20 | name String
21 | password String?
22 | picture String?
23 | isVerified Boolean @default(false)
24 | verifyCode String?
25 | verifyCodeExpiry DateTime?
26 | joinedAt DateTime @default(now())
27 | createdDocs Document[] @relation("CreatedDocuments")
28 |
29 | documents UserOnDocument[]
30 | }
31 |
32 | model Document {
33 | id String @id @default(uuid())
34 | name String @default("Untitled Document")
35 | data String
36 | createdAt DateTime @default(now())
37 | updatedAt DateTime @default(now())
38 | userId String
39 | createdBy User @relation(fields: [userId], references: [id], name: "CreatedDocuments")
40 | thumbnail String?
41 | deleteUrl String?
42 |
43 | users UserOnDocument[]
44 | }
45 |
46 | model UserOnDocument {
47 | userId String
48 | user User @relation(fields: [userId], references: [id])
49 | documentId String
50 | document Document @relation(fields: [documentId], references: [id], onDelete: Cascade)
51 |
52 | assignedAt DateTime @default(now())
53 |
54 | @@id([userId, documentId])
55 | }
56 |
57 | // model OTP {
58 | // id String @id @default(uuid())
59 | // email String
60 | // otp String
61 | // expiryDate DateTime
62 | // }
63 |
--------------------------------------------------------------------------------
/public/110045644.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/git-init-priyanshu/Docx/74239b9d003fb588ce9402ed740566aaef0c15e3/public/110045644.png
--------------------------------------------------------------------------------
/public/Forgot password.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/git-init-priyanshu/Docx/74239b9d003fb588ce9402ed740566aaef0c15e3/public/Forgot password.webp
--------------------------------------------------------------------------------
/public/Hero image mobile view.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/git-init-priyanshu/Docx/74239b9d003fb588ce9402ed740566aaef0c15e3/public/Hero image mobile view.webp
--------------------------------------------------------------------------------
/public/Hero video thumbnail.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/git-init-priyanshu/Docx/74239b9d003fb588ce9402ed740566aaef0c15e3/public/Hero video thumbnail.png
--------------------------------------------------------------------------------
/public/Hero_section_image.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/git-init-priyanshu/Docx/74239b9d003fb588ce9402ed740566aaef0c15e3/public/Hero_section_image.png
--------------------------------------------------------------------------------
/public/Reset password.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/git-init-priyanshu/Docx/74239b9d003fb588ce9402ed740566aaef0c15e3/public/Reset password.webp
--------------------------------------------------------------------------------
/public/Signup.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/git-init-priyanshu/Docx/74239b9d003fb588ce9402ed740566aaef0c15e3/public/Signup.webp
--------------------------------------------------------------------------------
/public/Toolbar.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/git-init-priyanshu/Docx/74239b9d003fb588ce9402ed740566aaef0c15e3/public/Toolbar.png
--------------------------------------------------------------------------------
/public/Verify otp.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/git-init-priyanshu/Docx/74239b9d003fb588ce9402ed740566aaef0c15e3/public/Verify otp.webp
--------------------------------------------------------------------------------
/public/ai-feature.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/git-init-priyanshu/Docx/74239b9d003fb588ce9402ed740566aaef0c15e3/public/ai-feature.png
--------------------------------------------------------------------------------
/public/collab-feature.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/git-init-priyanshu/Docx/74239b9d003fb588ce9402ed740566aaef0c15e3/public/collab-feature.png
--------------------------------------------------------------------------------
/public/createdoc.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/git-init-priyanshu/Docx/74239b9d003fb588ce9402ed740566aaef0c15e3/public/createdoc.png
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/git-init-priyanshu/Docx/74239b9d003fb588ce9402ed740566aaef0c15e3/public/favicon.ico
--------------------------------------------------------------------------------
/public/github.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/git-init-priyanshu/Docx/74239b9d003fb588ce9402ed740566aaef0c15e3/public/github.png
--------------------------------------------------------------------------------
/public/google_icon.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/public/grid_bg.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/public/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/git-init-priyanshu/Docx/74239b9d003fb588ce9402ed740566aaef0c15e3/public/logo.png
--------------------------------------------------------------------------------
/public/mask.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/public/profilepic_placeholder.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/git-init-priyanshu/Docx/74239b9d003fb588ce9402ed740566aaef0c15e3/public/profilepic_placeholder.png
--------------------------------------------------------------------------------
/public/signin.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/git-init-priyanshu/Docx/74239b9d003fb588ce9402ed740566aaef0c15e3/public/signin.webp
--------------------------------------------------------------------------------
/tailwind.config.ts:
--------------------------------------------------------------------------------
1 | import type { Config } from "tailwindcss";
2 |
3 | const {
4 | default: flattenColorPalette,
5 | } = require("tailwindcss/lib/util/flattenColorPalette");
6 |
7 | const config = {
8 | darkMode: ["class"],
9 | content: [
10 | "./pages/**/*.{ts,tsx}",
11 | "./components/**/*.{ts,tsx}",
12 | "./app/**/*.{ts,tsx}",
13 | "./src/**/*.{ts,tsx}",
14 | ],
15 | prefix: "",
16 | theme: {
17 | container: {
18 | center: true,
19 | padding: "2rem",
20 | screens: {
21 | "2xl": "1400px",
22 | },
23 | },
24 | extend: {
25 | colors: {
26 | border: "hsl(var(--border))",
27 | input: "hsl(var(--input))",
28 | ring: "hsl(var(--ring))",
29 | background: "hsl(var(--background))",
30 | foreground: "hsl(var(--foreground))",
31 | primary: {
32 | DEFAULT: "hsl(var(--primary))",
33 | foreground: "hsl(var(--primary-foreground))",
34 | },
35 | secondary: {
36 | DEFAULT: "hsl(var(--secondary))",
37 | foreground: "hsl(var(--secondary-foreground))",
38 | },
39 | destructive: {
40 | DEFAULT: "hsl(var(--destructive))",
41 | foreground: "hsl(var(--destructive-foreground))",
42 | },
43 | muted: {
44 | DEFAULT: "hsl(var(--muted))",
45 | foreground: "hsl(var(--muted-foreground))",
46 | },
47 | accent: {
48 | DEFAULT: "hsl(var(--accent))",
49 | foreground: "hsl(var(--accent-foreground))",
50 | },
51 | popover: {
52 | DEFAULT: "hsl(var(--popover))",
53 | foreground: "hsl(var(--popover-foreground))",
54 | },
55 | card: {
56 | DEFAULT: "hsl(var(--card))",
57 | foreground: "hsl(var(--card-foreground))",
58 | },
59 | },
60 | borderRadius: {
61 | lg: "var(--radius)",
62 | md: "calc(var(--radius) - 2px)",
63 | sm: "calc(var(--radius) - 4px)",
64 | },
65 | keyframes: {
66 | aurora: {
67 | from: {
68 | backgroundPosition: "50% 50%, 50% 50%",
69 | },
70 | to: {
71 | backgroundPosition: "350% 50%, 350% 50%",
72 | },
73 | },
74 | "accordion-down": {
75 | from: { height: "0" },
76 | to: { height: "var(--radix-accordion-content-height)" },
77 | },
78 | "accordion-up": {
79 | from: { height: "var(--radix-accordion-content-height)" },
80 | to: { height: "0" },
81 | },
82 | },
83 | animation: {
84 | aurora: "aurora 60s linear infinite",
85 | "accordion-down": "accordion-down 0.2s ease-out",
86 | "accordion-up": "accordion-up 0.2s ease-out",
87 | },
88 | backgroundImage: {
89 | "gradient-radial":
90 | "radial-gradient(circle at center, var(--tw-gradient-stops))",
91 | },
92 | },
93 | },
94 | plugins: [
95 | require("tailwindcss-animate"),
96 | require("@tailwindcss/typography"),
97 | addVariablesForColors,
98 | ],
99 | } satisfies Config;
100 |
101 | // This plugin adds each Tailwind color as a global CSS variable, e.g. var(--gray-200).
102 | function addVariablesForColors({ addBase, theme }: any) {
103 | let allColors = flattenColorPalette(theme("colors"));
104 | let newVars = Object.fromEntries(
105 | Object.entries(allColors).map(([key, val]) => [`--${key}`, val]),
106 | );
107 |
108 | addBase({
109 | ":root": newVars,
110 | });
111 | }
112 |
113 | export default config;
114 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "lib": ["dom", "dom.iterable", "esnext"],
4 | "allowJs": true,
5 | "skipLibCheck": true,
6 | "strict": true,
7 | "noEmit": true,
8 | "esModuleInterop": true,
9 | "module": "esnext",
10 | "moduleResolution": "bundler",
11 | "resolveJsonModule": true,
12 | "isolatedModules": true,
13 | "jsx": "preserve",
14 | "incremental": true,
15 | "plugins": [
16 | {
17 | "name": "next"
18 | }
19 | ],
20 | "paths": {
21 | "@/*": ["./*"]
22 | }
23 | },
24 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
25 | "exclude": ["node_modules"]
26 | }
27 |
--------------------------------------------------------------------------------