├── .env.example
├── .eslintrc
├── .github
└── workflows
│ ├── codeql-analysis.yml
│ ├── images.yml
│ └── notfoundbot.yml
├── .gitignore
├── LICENSE
├── README.md
├── SECURITY.md
├── next-env.d.ts
├── next.config.js
├── package.json
├── postcss.config.js
├── public
├── icons
│ └── favicon.ico
├── images
│ ├── banner.png
│ └── docs-blank.png
└── screenshots
│ ├── editor.PNG
│ └── home.PNG
├── src
├── components
│ ├── buttons
│ │ └── Button.tsx
│ ├── header
│ │ ├── Header.tsx
│ │ ├── HeaderLeft.tsx
│ │ ├── HeaderRight.tsx
│ │ └── HeaderSearch.tsx
│ ├── icon
│ │ ├── Icon.tsx
│ │ └── IconButton.tsx
│ ├── modals
│ │ └── Modal.tsx
│ └── wrappers
│ │ └── DefaultWrapper.tsx
├── configs
│ └── firebase.ts
├── features
│ ├── auth
│ │ ├── IsAuth.tsx
│ │ ├── IsNotAuth.tsx
│ │ └── LoginPage.tsx
│ ├── document
│ │ ├── editor
│ │ │ ├── EditorHeader.tsx
│ │ │ ├── EditorPage.tsx
│ │ │ └── TextEditor.tsx
│ │ ├── recent
│ │ │ ├── DocumentsSection.tsx
│ │ │ ├── RecentDocument.tsx
│ │ │ └── RecentDocuments.tsx
│ │ └── start
│ │ │ ├── BlankDocument.tsx
│ │ │ ├── CreateDocument.tsx
│ │ │ └── StartDocumentSection.tsx
│ └── home
│ │ └── HomePage.tsx
├── pages
│ ├── 404.tsx
│ ├── _app.tsx
│ ├── _document.tsx
│ ├── api
│ │ └── auth
│ │ │ └── [...nextauth].ts
│ ├── doc
│ │ └── [id].tsx
│ ├── index.tsx
│ └── login.tsx
├── services
│ └── documentServices.tsx
├── styles
│ └── globals.css
└── types
│ └── env.d.ts
├── tailwind.config.js
├── tsconfig.json
└── yarn.lock
/.env.example:
--------------------------------------------------------------------------------
1 | # Firebase
2 | NEXT_PUBLIC_FIREBASE_API_KEY=
3 | NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN=
4 | NEXT_PUBLIC_FIREBASE_PROJECT_ID=
5 | NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET=
6 | NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID=
7 | NEXT_PUBLIC_FIREBASE_APP_ID=
8 | NEXT_PUBLIC_FIREBASE_MEASUREMENT_ID=
9 |
10 | # Authentication
11 | GOOGLE_CLIENT_ID=
12 | GOOGLE_CLIENT_SECRET=
13 | NEXTAUTH_URL=
14 | NEXTAUTH_SECRET=
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "extends": ["next", "next/core-web-vitals"]
3 | }
4 |
--------------------------------------------------------------------------------
/.github/workflows/codeql-analysis.yml:
--------------------------------------------------------------------------------
1 | # For most projects, this workflow file will not need changing; you simply need
2 | # to commit it to your repository.
3 | #
4 | # You may wish to alter this file to override the set of languages analyzed,
5 | # or to provide custom queries or build logic.
6 | name: "CodeQL"
7 |
8 | on:
9 | push:
10 | branches: [main]
11 | pull_request:
12 | # The branches below must be a subset of the branches above
13 | branches: [main]
14 | schedule:
15 | - cron: "0 13 * * 3"
16 |
17 | jobs:
18 | analyze:
19 | name: Analyze
20 | runs-on: ubuntu-latest
21 |
22 | strategy:
23 | fail-fast: false
24 | matrix:
25 | # Override automatic language detection by changing the below list
26 | # Supported options are ['csharp', 'cpp', 'go', 'java', 'javascript', 'python']
27 | language: ["javascript"]
28 | # Learn more...
29 | # https://docs.github.com/en/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#overriding-automatic-language-detection
30 |
31 | steps:
32 | - name: Checkout repository
33 | uses: actions/checkout@v2
34 | with:
35 | # We must fetch at least the immediate parents so that if this is
36 | # a pull request then we can checkout the head.
37 | fetch-depth: 2
38 |
39 | # If this run was triggered by a pull request event, then checkout
40 | # the head of the pull request instead of the merge commit.
41 | - run: git checkout HEAD^2
42 | if: ${{ github.event_name == 'pull_request' }}
43 |
44 | # Initializes the CodeQL tools for scanning.
45 | - name: Initialize CodeQL
46 | uses: github/codeql-action/init@v1
47 | with:
48 | languages: ${{ matrix.language }}
49 | # If you wish to specify custom queries, you can do so here or in a config file.
50 | # By default, queries listed here will override any specified in a config file.
51 | # Prefix the list here with "+" to use these queries and those in the config file.
52 | # queries: ./path/to/local/query, your-org/your-repo/queries@main
53 |
54 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
55 | # If this step fails, then you should remove it and run the build manually (see below)
56 | - name: Autobuild
57 | uses: github/codeql-action/autobuild@v1
58 |
59 | # ℹ️ Command-line programs to run using the OS shell.
60 | # 📚 https://git.io/JvXDl
61 |
62 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
63 | # and modify them (or add more) to build your code if your project
64 | # uses a compiled language
65 |
66 | #- run: |
67 | # make bootstrap
68 | # make release
69 |
70 | - name: Perform CodeQL Analysis
71 | uses: github/codeql-action/analyze@v1
72 |
--------------------------------------------------------------------------------
/.github/workflows/images.yml:
--------------------------------------------------------------------------------
1 | name: Compress Images
2 | on:
3 | pull_request:
4 | # Run Image Actions when JPG, JPEG, PNG or WebP files are added or changed.
5 | # See https://help.github.com/en/actions/automating-your-workflow-with-github-actions/workflow-syntax-for-github-actions#onpushpull_requestpaths for reference.
6 | paths:
7 | - "**.jpg"
8 | - "**.jpeg"
9 | - "**.png"
10 | - "**.webp"
11 | jobs:
12 | build:
13 | # Only run on Pull Requests within the same repository, and not from forks.
14 | if: github.event.pull_request.head.repo.full_name == github.repository
15 | name: calibreapp/image-actions
16 | runs-on: ubuntu-latest
17 | steps:
18 | - name: Checkout Repo
19 | uses: actions/checkout@v2
20 |
21 | - name: Compress Images
22 | uses: calibreapp/image-actions@main
23 | with:
24 | # The `GITHUB_TOKEN` is automatically generated by GitHub and scoped only to the repository that is currently running the action. By default, the action can’t update Pull Requests initiated from forked repositories.
25 | # See https://docs.github.com/en/actions/reference/authentication-in-a-workflow and https://help.github.com/en/articles/virtual-environments-for-github-actions#token-permissions
26 | githubToken: ${{ secrets.GITHUB_TOKEN }}
27 |
--------------------------------------------------------------------------------
/.github/workflows/notfoundbot.yml:
--------------------------------------------------------------------------------
1 | name: notfoundbot
2 | on:
3 | schedule:
4 | - cron: "0 5 * * *"
5 | jobs:
6 | check:
7 | runs-on: ubuntu-latest
8 | steps:
9 | - uses: actions/checkout@v2
10 | - name: Fix links
11 | uses: tmcw/notfoundbot@v2.0.0-beta.1
12 | env:
13 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
14 |
--------------------------------------------------------------------------------
/.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 |
8 | # testing
9 | /coverage
10 |
11 | # next.js
12 | /.next/
13 | /out/
14 |
15 | # production
16 | /build
17 |
18 | # misc
19 | .DS_Store
20 | *.pem
21 |
22 | # debug
23 | npm-debug.log*
24 | yarn-debug.log*
25 | yarn-error.log*
26 |
27 | # local env files
28 | .env.local
29 | .env.development.local
30 | .env.test.local
31 | .env.production.local
32 |
33 | # vercel
34 | .vercel
35 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 Martin Velkov
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Google Docs Clone
7 |
8 |
9 |
10 |
11 | Google Docs Clone created with Next.JS.
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 | Demo •
22 | Key Features •
23 | Key Technologies •
24 | Setup •
25 | Support •
26 | License
27 |
28 |
29 | 
30 |
31 | ---
32 |
33 | 
34 |
35 | ---
36 |
37 | ## Demo
38 | Here is a working live demo [here](https://google-docs-clone-martstech.vercel.app/)
39 |
40 | ---
41 |
42 | ## Key Features
43 |
44 | - Rich Text Editor
45 | - Documents stored real time
46 | - Autosave
47 | - Authentication
48 | - Responsive Design
49 |
50 | ---
51 |
52 | ## Key Technologies
53 |
54 | - Next.JS
55 | - Firebase
56 | - TailwindCSS
57 | - NextAuth
58 | - Typescript
59 |
60 | ---
61 |
62 | ## Setup
63 |
64 | Clone this repo to your desktop and run `yarn install` to install all the dependencies.
65 | Then run `yarn dev` to start the application locally
66 |
67 | Change the .env.example file to .env.local and fill the empty fields
68 |
69 | ---
70 |
71 | ## Support
72 |
73 | Whether you use this project, have learned something from it, or just like it, please consider supporting it by buying me a coffee, so I can dedicate more time on open-source projects like this :)
74 |
75 |
76 |
77 |
78 |
79 | ---
80 |
81 | ## License
82 |
83 | >You can check out the full license [here](https://github.com/MartsTech/google-docs-clone/blob/main/LICENSE)
84 |
85 | This project is licensed under the terms of the **MIT** license.
86 |
--------------------------------------------------------------------------------
/SECURITY.md:
--------------------------------------------------------------------------------
1 | # Security Policy
2 |
3 | ## Reporting a Vulnerability
4 |
5 | DM me on the contacts provided in my profile page: https://github.com/MartsTech
6 |
--------------------------------------------------------------------------------
/next-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | ///
3 | ///
4 |
5 | // NOTE: This file should not be edited
6 | // see https://nextjs.org/docs/basic-features/typescript for more information.
7 |
--------------------------------------------------------------------------------
/next.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | reactStrictMode: true,
3 | }
4 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "google-docs-clone",
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 | "gen-env-types": "npx gen-env-types .env.local -o src/types/env.d.ts -e ."
11 | },
12 | "dependencies": {
13 | "@material-tailwind/react": "0.3.4",
14 | "@next-auth/firebase-adapter": "^0.1.3",
15 | "draft-js": "^0.11.7",
16 | "firebase": "^8.10.0",
17 | "moment": "^2.29.1",
18 | "next": "12.0.4",
19 | "next-auth": "^4.0.2",
20 | "react": "17.0.2",
21 | "react-dom": "17.0.2",
22 | "react-draft-wysiwyg": "^1.14.7",
23 | "react-firebase-hooks": "^3.0.4"
24 | },
25 | "devDependencies": {
26 | "@types/draft-js": "^0.11.7",
27 | "@types/react": "^17.0.37",
28 | "autoprefixer": "^10.4.0",
29 | "eslint": "7.32.0",
30 | "eslint-config-next": "12.0.4",
31 | "gen-env-types": "^1.3.0",
32 | "postcss": "^8.4.4",
33 | "tailwindcss": "^2.2.19",
34 | "typescript": "^4.5.2"
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | }
7 |
--------------------------------------------------------------------------------
/public/icons/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MartsTech/google-docs-clone/255bac4f6139c2c408d7b26b47c461489a100356/public/icons/favicon.ico
--------------------------------------------------------------------------------
/public/images/banner.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MartsTech/google-docs-clone/255bac4f6139c2c408d7b26b47c461489a100356/public/images/banner.png
--------------------------------------------------------------------------------
/public/images/docs-blank.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MartsTech/google-docs-clone/255bac4f6139c2c408d7b26b47c461489a100356/public/images/docs-blank.png
--------------------------------------------------------------------------------
/public/screenshots/editor.PNG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MartsTech/google-docs-clone/255bac4f6139c2c408d7b26b47c461489a100356/public/screenshots/editor.PNG
--------------------------------------------------------------------------------
/public/screenshots/home.PNG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MartsTech/google-docs-clone/255bac4f6139c2c408d7b26b47c461489a100356/public/screenshots/home.PNG
--------------------------------------------------------------------------------
/src/components/buttons/Button.tsx:
--------------------------------------------------------------------------------
1 | // @ts-ignore
2 | import MaterialButton from "@material-tailwind/react/Button";
3 |
4 | interface ButtonProps {
5 | type: "filled" | "link";
6 | ripple: "light" | "dark";
7 | color: string;
8 | onClick?: () => void;
9 | className?: string;
10 | }
11 |
12 | const Button: React.FC = ({
13 | children,
14 | type,
15 | ripple,
16 | color,
17 | onClick,
18 | className,
19 | }) => {
20 | return (
21 |
28 | {children}
29 |
30 | );
31 | };
32 |
33 | export default Button;
34 |
--------------------------------------------------------------------------------
/src/components/header/Header.tsx:
--------------------------------------------------------------------------------
1 | import HeaderLeft from "./HeaderLeft";
2 | import HeaderRight from "./HeaderRight";
3 | import HeaderSearch from "./HeaderSearch";
4 |
5 | interface HeaderProps {}
6 |
7 | const Header: React.FC = ({}) => {
8 | return (
9 |
17 | );
18 | };
19 |
20 | export default Header;
21 |
--------------------------------------------------------------------------------
/src/components/header/HeaderLeft.tsx:
--------------------------------------------------------------------------------
1 | import Icon from "@component/icon/Icon";
2 | import IconButton from "@component/icon/IconButton";
3 |
4 | interface HeaderLeftProps {}
5 |
6 | const HeaderLeft: React.FC = () => {
7 | return (
8 | <>
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
20 | Docs
21 |
22 | >
23 | );
24 | };
25 |
26 | export default HeaderLeft;
27 |
--------------------------------------------------------------------------------
/src/components/header/HeaderRight.tsx:
--------------------------------------------------------------------------------
1 | import Icon from "@component/icon/Icon";
2 | import IconButton from "@component/icon/IconButton";
3 | import { signOut, useSession } from "next-auth/react";
4 |
5 | interface HeaderRightProps {}
6 |
7 | const HeaderRight: React.FC = () => {
8 | const session = useSession();
9 |
10 | return (
11 | <>
12 |
16 |
17 |
18 |
19 | {/* eslint-disable-next-line @next/next/no-img-element*/}
20 |
signOut()}
25 | alt="avatar"
26 | />
27 | >
28 | );
29 | };
30 |
31 | export default HeaderRight;
32 |
--------------------------------------------------------------------------------
/src/components/header/HeaderSearch.tsx:
--------------------------------------------------------------------------------
1 | import Icon from "@component/icon/Icon";
2 |
3 | interface HeaderSearchProps {}
4 |
5 | const HeaderSearch: React.FC = () => {
6 | return (
7 |
12 |
13 |
18 |
19 | );
20 | };
21 |
22 | export default HeaderSearch;
23 |
--------------------------------------------------------------------------------
/src/components/icon/Icon.tsx:
--------------------------------------------------------------------------------
1 | // @ts-ignore
2 | import MaterialIcon from "@material-tailwind/react/Icon";
3 |
4 | interface IconProps {
5 | name: string;
6 | size: string;
7 | color?: string;
8 | }
9 |
10 | const Icon: React.FC = ({ name, size, color }) => {
11 | return ;
12 | };
13 |
14 | export default Icon;
15 |
--------------------------------------------------------------------------------
/src/components/icon/IconButton.tsx:
--------------------------------------------------------------------------------
1 | // @ts-ignore
2 | import Button from "@material-tailwind/react/Button";
3 |
4 | interface ButtonProps {
5 | rounded?: boolean;
6 | className?: string;
7 | }
8 |
9 | const IconButton: React.FC = ({
10 | children,
11 | rounded = false,
12 | className,
13 | }) => {
14 | return (
15 |
25 | );
26 | };
27 |
28 | export default IconButton;
29 |
--------------------------------------------------------------------------------
/src/components/modals/Modal.tsx:
--------------------------------------------------------------------------------
1 | // @ts-ignore
2 | import MaterialModal from "@material-tailwind/react/Modal";
3 | // @ts-ignore
4 | import ModalBody from "@material-tailwind/react/ModalBody";
5 | // @ts-ignore
6 | import ModalFooter from "@material-tailwind/react/ModalFooter";
7 |
8 | interface ModalProps {
9 | active: boolean;
10 | onClose: () => void;
11 | Body: JSX.Element;
12 | Footer: JSX.Element;
13 | }
14 |
15 | const Modal: React.FC = ({ active, onClose, Body, Footer }) => {
16 | return (
17 |
18 | {Body}
19 | {Footer}
20 |
21 | );
22 | };
23 |
24 | export default Modal;
25 |
--------------------------------------------------------------------------------
/src/components/wrappers/DefaultWrapper.tsx:
--------------------------------------------------------------------------------
1 | interface DefaultWrapperProps {}
2 |
3 | const DefaultWrapper: React.FC = ({ children }) => {
4 | return {children}
;
5 | };
6 |
7 | export default DefaultWrapper;
8 |
--------------------------------------------------------------------------------
/src/configs/firebase.ts:
--------------------------------------------------------------------------------
1 | import firebase from "firebase/app";
2 | import "firebase/firestore";
3 |
4 | const firebaseConfig = {
5 | apiKey: process.env.NEXT_PUBLIC_FIREBASE_API_KEY,
6 | authDomain: process.env.NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN,
7 | projectId: process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID,
8 | storageBucket: process.env.NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET,
9 | messagingSenderId: process.env.NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID,
10 | appId: process.env.NEXT_PUBLIC_FIREBASE_APP_ID,
11 | measurementId: process.env.NEXT_PUBLIC_FIREBASE_MEASUREMENT_ID,
12 | };
13 |
14 | const firebaseApp = !firebase.apps.length
15 | ? firebase.initializeApp(firebaseConfig)
16 | : firebase.app();
17 |
18 | const db = firebaseApp.firestore();
19 |
20 | export { db };
21 |
22 |
--------------------------------------------------------------------------------
/src/features/auth/IsAuth.tsx:
--------------------------------------------------------------------------------
1 | import { useSession } from "next-auth/react";
2 | import { useRouter } from "next/router";
3 | import { useEffect } from "react";
4 |
5 | interface IsAuthProps {}
6 |
7 | const IsAuth: React.FC = ({ children }) => {
8 | const session = useSession();
9 | const router = useRouter();
10 |
11 | useEffect(() => {
12 | if (session.status === "unauthenticated") {
13 | router.replace("/login");
14 | }
15 | }, [session, router]);
16 |
17 | return <>{children}>;
18 | };
19 |
20 | export default IsAuth;
21 |
--------------------------------------------------------------------------------
/src/features/auth/IsNotAuth.tsx:
--------------------------------------------------------------------------------
1 | import { useSession } from "next-auth/react";
2 | import { useRouter } from "next/router";
3 | import { useEffect } from "react";
4 |
5 | interface IsNotAuthProps {}
6 |
7 | const IsNotAuth: React.FC = ({ children }) => {
8 | const session = useSession();
9 | const router = useRouter();
10 |
11 | useEffect(() => {
12 | if (session.status === "authenticated") {
13 | router.replace("/");
14 | }
15 | }, [session, router]);
16 |
17 | return <>{children}>;
18 | };
19 |
20 | export default IsNotAuth;
21 |
--------------------------------------------------------------------------------
/src/features/auth/LoginPage.tsx:
--------------------------------------------------------------------------------
1 | import Button from "@component/buttons/Button";
2 | import { signIn } from "next-auth/react";
3 | import Image from "next/image";
4 |
5 | interface LoginPageProps {}
6 |
7 | const LoginPage: React.FC = () => {
8 | return (
9 |
13 |
20 |
29 |
33 | Disclaimer: This is not the official Google Docs. It is
34 | a redesign, built purely for educational purpose.
35 |
36 |
37 | );
38 | };
39 |
40 | export default LoginPage;
41 |
--------------------------------------------------------------------------------
/src/features/document/editor/EditorHeader.tsx:
--------------------------------------------------------------------------------
1 | import Button from "@component/buttons/Button";
2 | import Icon from "@component/icon/Icon";
3 | import { useSession } from "next-auth/react";
4 | import { useRouter } from "next/router";
5 |
6 | interface EditorHeaderProps {
7 | filename: string;
8 | }
9 |
10 | const EditorHeader: React.FC = ({ filename }) => {
11 | const router = useRouter();
12 | const session = useSession();
13 |
14 | return (
15 |
16 | router.push("/")} className="cursor-pointer">
17 |
18 |
19 |
20 |
{filename}
21 |
25 |
File
26 |
Edit
27 |
View
28 |
Insert
29 |
Format
30 |
Tools
31 |
32 |
33 |
42 |
43 | {/*eslint-disable-next-line @next/next/no-img-element*/}
44 |
49 |
50 | );
51 | };
52 |
53 | export default EditorHeader;
54 |
--------------------------------------------------------------------------------
/src/features/document/editor/EditorPage.tsx:
--------------------------------------------------------------------------------
1 | import firebase from "firebase";
2 | import EditorHeader from "./EditorHeader";
3 | import TextEditor from "./TextEditor";
4 |
5 | interface EditorPageProps {
6 | snapshot:
7 | | firebase.firestore.DocumentSnapshot
8 | | undefined;
9 | }
10 |
11 | const EditorPage: React.FC = ({ snapshot }) => {
12 | return (
13 | <>
14 |
15 |
16 | >
17 | );
18 | };
19 |
20 | export default EditorPage;
21 |
--------------------------------------------------------------------------------
/src/features/document/editor/TextEditor.tsx:
--------------------------------------------------------------------------------
1 | import { db } from "@config/firebase";
2 | import { convertFromRaw, convertToRaw, EditorState } from "draft-js";
3 | import { useSession } from "next-auth/react";
4 | import dynamic from "next/dynamic";
5 | import { useRouter } from "next/router";
6 | import { useEffect, useState } from "react";
7 | import "react-draft-wysiwyg/dist/react-draft-wysiwyg.css";
8 | import { useDocumentOnce } from "react-firebase-hooks/firestore";
9 |
10 | const Editor = dynamic(
11 | // @ts-ignore
12 | () => import("react-draft-wysiwyg").then((module) => module.Editor),
13 | { ssr: false }
14 | );
15 |
16 | interface TextEditorProps {}
17 |
18 | const TextEditor: React.FC = () => {
19 | const [editorState, setEditorState] = useState(EditorState.createEmpty());
20 |
21 | const session = useSession();
22 |
23 | const router = useRouter();
24 | const { id } = router.query;
25 |
26 | const [snapshot] = useDocumentOnce(
27 | db
28 | .collection("userDocs")
29 | .doc(session?.data?.user?.email as string)
30 | .collection("docs")
31 | .doc(id as string)
32 | );
33 |
34 | useEffect(() => {
35 | if (snapshot?.data()?.editorState) {
36 | setEditorState(
37 | EditorState.createWithContent(
38 | convertFromRaw(snapshot?.data()?.editorState)
39 | )
40 | );
41 | }
42 | }, [snapshot]);
43 |
44 | const onEditorStateChange = (editorState: EditorState) => {
45 | setEditorState(editorState);
46 |
47 | db.collection("userDocs")
48 | .doc(session?.data?.user?.email as string)
49 | .collection("docs")
50 | .doc(id as string)
51 | .set(
52 | {
53 | editorState: convertToRaw(editorState.getCurrentContent()),
54 | },
55 | {
56 | merge: true,
57 | }
58 | );
59 | };
60 |
61 | return (
62 |
63 | {/*@ts-ignore*/}
64 |
73 |
74 | );
75 | };
76 |
77 | export default TextEditor;
78 |
--------------------------------------------------------------------------------
/src/features/document/recent/DocumentsSection.tsx:
--------------------------------------------------------------------------------
1 | import Icon from "@component/icon/Icon";
2 | import RecentDocuments from "./RecentDocuments";
3 |
4 | interface DocumentsSectionProps {}
5 |
6 | const DocumentsSection: React.FC = () => {
7 | return (
8 |
9 |
10 |
11 |
My Documents
12 |
Date Created
13 |
14 |
15 |
16 |
17 |
18 | );
19 | };
20 |
21 | export default DocumentsSection;
22 |
--------------------------------------------------------------------------------
/src/features/document/recent/RecentDocument.tsx:
--------------------------------------------------------------------------------
1 | import Icon from "@component/icon/Icon";
2 | import IconButton from "@component/icon/IconButton";
3 | import { useRouter } from "next/router";
4 |
5 | interface RecentDocumentProps {
6 | id: string;
7 | filename: string;
8 | date: string;
9 | }
10 |
11 | const RecentDocument: React.FC = ({
12 | id,
13 | filename,
14 | date,
15 | }) => {
16 | const router = useRouter();
17 |
18 | return (
19 | router.push(`/doc/${id}`)}
23 | >
24 |
25 |
{filename}
26 |
{date}
27 |
28 |
29 |
30 |
31 | );
32 | };
33 |
34 | export default RecentDocument;
35 |
--------------------------------------------------------------------------------
/src/features/document/recent/RecentDocuments.tsx:
--------------------------------------------------------------------------------
1 | import { db } from "@config/firebase";
2 | import moment from "moment";
3 | import { useSession } from "next-auth/react";
4 | import { useCollectionOnce } from "react-firebase-hooks/firestore";
5 | import RecentDocument from "./RecentDocument";
6 |
7 | interface RecentDocumentsProps {}
8 |
9 | const RecentDocuments: React.FC = ({}) => {
10 | const session = useSession();
11 | const [snapshot] = useCollectionOnce(
12 | db
13 | .collection("userDocs")
14 | .doc(session?.data?.user?.email as string)
15 | .collection("docs")
16 | .orderBy("timestamp", "desc")
17 | );
18 |
19 | return (
20 | <>
21 | {snapshot?.docs.map((doc) => (
22 |
28 | ))}
29 | >
30 | );
31 | };
32 |
33 | export default RecentDocuments;
34 |
--------------------------------------------------------------------------------
/src/features/document/start/BlankDocument.tsx:
--------------------------------------------------------------------------------
1 | import Image from "next/image";
2 | import { useState } from "react";
3 | import CreateDocument from "./CreateDocument";
4 |
5 | interface BlankDocumentProps {}
6 |
7 | const BlankDocument: React.FC = () => {
8 | const [showModal, setShowModal] = useState(false);
9 |
10 | return (
11 | <>
12 | setShowModal(false)} />
13 | setShowModal(!showModal)}
18 | >
19 |
20 |
21 | Blank
22 | >
23 | );
24 | };
25 |
26 | export default BlankDocument;
27 |
--------------------------------------------------------------------------------
/src/features/document/start/CreateDocument.tsx:
--------------------------------------------------------------------------------
1 | import Button from "@component/buttons/Button";
2 | import Modal from "@component/modals/Modal";
3 | import { createDocument } from "@service/documentServices";
4 | import { useSession } from "next-auth/react";
5 | import { useState } from "react";
6 |
7 | interface CreateDocumentProps {
8 | showModal: boolean;
9 | close: () => void;
10 | }
11 |
12 | const CreateDocument: React.FC = ({
13 | showModal,
14 | close,
15 | }) => {
16 | const [input, setInput] = useState("");
17 | const session = useSession();
18 |
19 | const create = () => {
20 | createDocument(input, session?.data?.user?.email || "");
21 | setInput("");
22 | close();
23 | };
24 |
25 | return (
26 | setInput(e.target.value)}
33 | type="text"
34 | className="outline-none w-full"
35 | placeholder="Enter name of document..."
36 | onKeyDown={(e) => e.key === "Enter" && create()}
37 | />
38 | }
39 | Footer={
40 | <>
41 |
44 |
52 | >
53 | }
54 | />
55 | );
56 | };
57 |
58 | export default CreateDocument;
59 |
--------------------------------------------------------------------------------
/src/features/document/start/StartDocumentSection.tsx:
--------------------------------------------------------------------------------
1 | import Icon from "@component/icon/Icon";
2 | import IconButton from "@component/icon/IconButton";
3 | import BlankDocument from "./BlankDocument";
4 |
5 | interface StartDocumentSectionProps {}
6 |
7 | const StartDocumentSection: React.FC = () => {
8 | return (
9 |
10 |
11 |
Start a new document
12 |
13 |
14 |
15 |
16 |
17 |
18 | );
19 | };
20 |
21 | export default StartDocumentSection;
22 |
--------------------------------------------------------------------------------
/src/features/home/HomePage.tsx:
--------------------------------------------------------------------------------
1 | import Header from "@component/header/Header";
2 | import DefaultWrapper from "@component/wrappers/DefaultWrapper";
3 | import DocumentsSection from "@feature/document/recent/DocumentsSection";
4 | import StartDocumentSection from "@feature/document/start/StartDocumentSection";
5 |
6 | interface HomePageProps {}
7 |
8 | const HomePage: React.FC = () => {
9 | return (
10 | <>
11 |
12 |
13 |
14 |
15 |
16 | >
17 | );
18 | };
19 |
20 | export default HomePage;
21 |
--------------------------------------------------------------------------------
/src/pages/404.tsx:
--------------------------------------------------------------------------------
1 | import { useRouter } from "next/router";
2 | import { useEffect } from "react";
3 |
4 | const Custom404 = () => {
5 | const router = useRouter();
6 |
7 | useEffect(() => {
8 | router.replace("/");
9 | });
10 |
11 | return null;
12 | };
13 |
14 | export default Custom404;
15 |
--------------------------------------------------------------------------------
/src/pages/_app.tsx:
--------------------------------------------------------------------------------
1 | import "@material-tailwind/react/tailwind.css";
2 | import "@style/globals.css";
3 | import { SessionProvider } from "next-auth/react";
4 | import type { AppProps } from "next/app";
5 | import Head from "next/head";
6 |
7 | const App = ({ Component, pageProps: { session, ...pageProps } }: AppProps) => {
8 | return (
9 |
10 |
11 | Google Docs Clone
12 |
13 | {/*@ts-ignore*/}
14 |
15 |
16 | );
17 | };
18 |
19 | export default App;
20 |
--------------------------------------------------------------------------------
/src/pages/_document.tsx:
--------------------------------------------------------------------------------
1 | import NextDocument, { Html, Head, Main, NextScript } from "next/document";
2 |
3 | class Document extends NextDocument {
4 | render() {
5 | return (
6 |
7 |
8 |
12 |
16 |
17 |
18 |
19 |
20 |
21 |
25 |
26 |
27 |
31 |
32 |
33 |
34 |
35 |
36 |
37 | );
38 | }
39 | }
40 |
41 | export default Document;
42 |
--------------------------------------------------------------------------------
/src/pages/api/auth/[...nextauth].ts:
--------------------------------------------------------------------------------
1 | import { db } from "@config/firebase";
2 | import { FirebaseAdapter } from "@next-auth/firebase-adapter";
3 | import NextAuth from "next-auth";
4 | import GoogleProvider from "next-auth/providers/google";
5 |
6 | export default NextAuth({
7 | secret: process.env.NEXTAUTH_SECRET,
8 | providers: [
9 | GoogleProvider({
10 | clientId: process.env.GOOGLE_CLIENT_ID,
11 | clientSecret: process.env.GOOGLE_CLIENT_SECRET,
12 | }),
13 | ],
14 | adapter: FirebaseAdapter(db),
15 | });
16 |
--------------------------------------------------------------------------------
/src/pages/doc/[id].tsx:
--------------------------------------------------------------------------------
1 | import { db } from "@config/firebase";
2 | import IsAuth from "@feature/auth/IsAuth";
3 | import EditorPage from "@feature/document/editor/EditorPage";
4 | import { GetServerSideProps } from "next";
5 | import { getSession, useSession } from "next-auth/react";
6 | import Head from "next/head";
7 | import { useRouter } from "next/router";
8 | import { useDocumentOnce } from "react-firebase-hooks/firestore";
9 |
10 | interface DocProps {}
11 |
12 | const Doc: React.FC = () => {
13 | const session = useSession();
14 |
15 | const router = useRouter();
16 | const { id } = router.query;
17 |
18 | const [snapshot, loading] = useDocumentOnce(
19 | db
20 | .collection("userDocs")
21 | .doc(session?.data?.user?.email as string)
22 | .collection("docs")
23 | .doc(id as string)
24 | );
25 |
26 | if (!loading && !snapshot?.data()?.filename) {
27 | router.replace("/");
28 | }
29 |
30 | return (
31 |
32 |
33 | {snapshot?.data()?.filename}
34 |
35 |
36 |
37 | );
38 | };
39 |
40 | export default Doc;
41 |
42 | export const getServerSideProps: GetServerSideProps = async (context) => {
43 | const session = await getSession(context);
44 |
45 | return {
46 | props: {
47 | session,
48 | },
49 | };
50 | };
51 |
--------------------------------------------------------------------------------
/src/pages/index.tsx:
--------------------------------------------------------------------------------
1 | import IsAuth from "@feature/auth/IsAuth";
2 | import HomePage from "@feature/home/HomePage";
3 | import { GetServerSideProps } from "next";
4 | import { getSession } from "next-auth/react";
5 |
6 | interface HomeProps {}
7 |
8 | const Home: React.FC = () => {
9 | return (
10 |
11 |
12 |
13 | );
14 | };
15 |
16 | export default Home;
17 |
18 | export const getServerSideProps: GetServerSideProps = async (context) => {
19 | const session = await getSession(context);
20 |
21 | return {
22 | props: {
23 | session,
24 | },
25 | };
26 | };
27 |
--------------------------------------------------------------------------------
/src/pages/login.tsx:
--------------------------------------------------------------------------------
1 | import IsNotAuth from "features/auth/IsNotAuth";
2 | import LoginPage from "features/auth/LoginPage";
3 | import { GetServerSideProps } from "next";
4 | import { getSession } from "next-auth/react";
5 | import Head from "next/head";
6 |
7 | interface LoginProps {}
8 |
9 | const Login: React.FC = () => {
10 | return (
11 |
12 |
13 | Login
14 |
15 |
16 |
17 | );
18 | };
19 |
20 | export default Login;
21 |
22 | export const getServerSideProps: GetServerSideProps = async (context) => {
23 | const session = await getSession(context);
24 |
25 | return {
26 | props: {
27 | session,
28 | },
29 | };
30 | };
31 |
--------------------------------------------------------------------------------
/src/services/documentServices.tsx:
--------------------------------------------------------------------------------
1 | import { db } from "@config/firebase";
2 | import firebase from "firebase";
3 |
4 | export const createDocument = (filename: string, email: string) => {
5 | if (filename == "" || email == "") return;
6 |
7 | db.collection("userDocs").doc(email).collection("docs").add({
8 | filename,
9 | timestamp: firebase.firestore.FieldValue.serverTimestamp(),
10 | });
11 | };
12 |
--------------------------------------------------------------------------------
/src/styles/globals.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | @layer components {
6 | .option {
7 | @apply cursor-pointer hover:bg-gray-100 transition duration-200
8 | ease-out p-2 rounded-lg;
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/src/types/env.d.ts:
--------------------------------------------------------------------------------
1 | declare global {
2 | namespace NodeJS {
3 | interface ProcessEnv {
4 | NEXT_PUBLIC_FIREBASE_API_KEY: string;
5 | NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN: string;
6 | NEXT_PUBLIC_FIREBASE_PROJECT_ID: string;
7 | NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET: string;
8 | NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID: string;
9 | NEXT_PUBLIC_FIREBASE_APP_ID: string;
10 | NEXT_PUBLIC_FIREBASE_MEASUREMENT_ID: string;
11 | GOOGLE_CLIENT_ID: string;
12 | GOOGLE_CLIENT_SECRET: string;
13 | NEXTAUTH_URL: string;
14 | NEXTAUTH_SECRET: string;
15 | }
16 | }
17 | }
18 |
19 | export {}
20 |
--------------------------------------------------------------------------------
/tailwind.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | mode: "jit",
3 | purge: ["./src/**/*.tsx"],
4 | darkMode: "class",
5 | theme: {
6 | extend: {},
7 | },
8 | variants: {
9 | extend: {},
10 | },
11 | plugins: [],
12 | };
13 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "baseUrl": "src",
4 | "paths": {
5 | "@component/*": [
6 | "components/*"
7 | ],
8 | "@config/*": [
9 | "configs/*"
10 | ],
11 | "@feature/*": [
12 | "features/*"
13 | ],
14 | "@service/*": [
15 | "services/*"
16 | ],
17 | "@type/*": [
18 | "types/*"
19 | ],
20 | "@style/*": [
21 | "styles/*"
22 | ]
23 | },
24 | "outDir": "build/dist",
25 | "module": "esnext",
26 | "target": "es5",
27 | "lib": [
28 | "es6",
29 | "dom",
30 | "esnext.asynciterable"
31 | ],
32 | "sourceMap": true,
33 | "allowJs": true,
34 | "jsx": "preserve",
35 | "moduleResolution": "node",
36 | "rootDir": "src",
37 | "forceConsistentCasingInFileNames": true,
38 | "noImplicitReturns": true,
39 | "noImplicitThis": true,
40 | "noImplicitAny": true,
41 | "strictNullChecks": true,
42 | "suppressImplicitAnyIndexErrors": true,
43 | "noUnusedLocals": true,
44 | "skipLibCheck": true,
45 | "strict": false,
46 | "noEmit": true,
47 | "esModuleInterop": true,
48 | "resolveJsonModule": true,
49 | "isolatedModules": true,
50 | "incremental": true
51 | },
52 | "exclude": [
53 | "node_modules",
54 | "build",
55 | "scripts",
56 | "acceptance-tests",
57 | "webpack",
58 | "jest",
59 | "src/setupTests.ts"
60 | ],
61 | "include": [
62 | "next-env.d.ts",
63 | "**/*.ts",
64 | "**/*.tsx",
65 | "src/components/header/Header.tsx"
66 | ]
67 | }
68 |
--------------------------------------------------------------------------------