├── Dockerfile ├── README.md ├── components ├── Avatar.tsx ├── Button.tsx ├── Form.tsx ├── Header.tsx ├── ImageUpload.tsx ├── Input.tsx ├── Layout.tsx ├── Modal.tsx ├── NotificationsFeed.tsx ├── layout │ ├── FollowBar.tsx │ ├── Sidebar.tsx │ ├── SidebarItem.tsx │ ├── SidebarLogo.tsx │ └── SidebarTweetButton.tsx ├── modals │ ├── EditModal.tsx │ ├── LoginModal.tsx │ └── RegisterModal.tsx ├── posts │ ├── CommentFeed.tsx │ ├── CommentItem.tsx │ ├── PostFeed.tsx │ └── PostItem.tsx └── users │ ├── UserBio.tsx │ └── UserHero.tsx ├── docker-compose.yml ├── docker-compose.yml-og ├── hooks ├── useCurrentUser.ts ├── useEditModal.ts ├── useFollow.ts ├── useLike.ts ├── useLoginModal.ts ├── useNotifications.ts ├── usePost.ts ├── usePosts.ts ├── useRegisterModal.ts ├── useUser.ts └── useUsers.ts ├── libs ├── fetcher.ts ├── prismadb.ts └── serverAuth.ts ├── next.config.js ├── package-lock.json ├── package.json ├── pages ├── _app.tsx ├── api │ ├── auth │ │ └── [...nextauth].ts │ ├── comments.ts │ ├── current.ts │ ├── edit.ts │ ├── follow.ts │ ├── like.ts │ ├── notifications │ │ └── [userId].ts │ ├── posts │ │ ├── [postId].ts │ │ └── index.ts │ ├── register.ts │ └── users │ │ ├── [userId].ts │ │ └── index.ts ├── index.tsx ├── notifications.tsx ├── posts │ └── [postId].tsx ├── search.tsx └── users │ └── [userId].tsx ├── postcss.config.js ├── prisma └── schema.prisma ├── public ├── favicon.ico ├── images │ └── placeholder.png ├── next.svg ├── thirteen.svg └── vercel.svg ├── styles └── globals.css ├── tailwind.config.js └── tsconfig.json /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:14.21 2 | 3 | WORKDIR /usr/src/app 4 | 5 | ENV DATABASE_URL "mongodb://root:prisma@mongodb-primary:27017/test?authSource=admin&retryWrites=false" 6 | 7 | ENV NEXTAUTH_SECRET "secret" 8 | 9 | ENV NEXTAUTH_JWT_SECRET "secret" 10 | 11 | COPY package*.json ./ 12 | 13 | RUN npm install \ 14 | npm install prisma 15 | COPY . . 16 | 17 | RUN npx prisma generate 18 | 19 | CMD ["npm", "run", "dev"] 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## The original Full stack application is developed by https://github.com/AntonioErdeljac/twitter-clone. All the credit for the application development goes to him and I do not intend to take any credit for it. 2 | 3 | ## What I have done is Dockerized this app, which was a complicated task because it uses prisma with MongoDB, we need MongoDB replica sets to establish the connectivity between prisma and MongoDB. 4 | 5 | ## For the original app Antonio uses MongoDB Atlas and Vercel to deploy the app, but I wanted to dockerize it so that we can run it on our local machine and to experience what itactually takes to create a containerized app. It took some effort but it's done. 6 | 7 | ## Please feel free to check out the Dockerized version. 8 | 9 | 10 | 11 | # Build and Deploy: TWITTER clone - Chwitter with React, Tailwind, Next, Prisma, Mongo, NextAuth & Docker 12 | 13 | 14 | ![Fullstack Twitter Clone (2)](https://user-images.githubusercontent.com/23248726/224405420-03112a76-250a-4283-992c-60e235170678.png) 15 | 16 | 17 | This is a repository for a FullStack Twitter clone tutorial using React, NextJS, TailwindCSS & Prisma, MondoDB and Docker. 18 | 19 | 20 | 21 | 22 | ``` 23 | 24 | ### Start the app 25 | 26 | ```shell 27 | git clone https://github.com/mandeepsingh10/chwitter.git 28 | cd chwitter 29 | docker-compose up 30 | go to localhost:3000 to check the app. 31 | ``` 32 | -------------------------------------------------------------------------------- /components/Avatar.tsx: -------------------------------------------------------------------------------- 1 | import Image from "next/image"; 2 | import { useRouter } from "next/router"; 3 | import { useCallback } from "react"; 4 | 5 | import useUser from "@/hooks/useUser"; 6 | 7 | interface AvatarProps { 8 | userId: string; 9 | isLarge?: boolean; 10 | hasBorder?: boolean; 11 | } 12 | 13 | const Avatar: React.FC = ({ userId, isLarge, hasBorder }) => { 14 | const router = useRouter(); 15 | 16 | const { data: fetchedUser } = useUser(userId); 17 | 18 | const onClick = useCallback((event: any) => { 19 | event.stopPropagation(); 20 | 21 | const url = `/users/${userId}`; 22 | 23 | router.push(url); 24 | }, [router, userId]); 25 | 26 | return ( 27 |
39 | Avatar 49 |
50 | ); 51 | } 52 | 53 | export default Avatar; -------------------------------------------------------------------------------- /components/Button.tsx: -------------------------------------------------------------------------------- 1 | interface ButtonProps { 2 | label: string; 3 | secondary?: boolean; 4 | fullWidth?: boolean; 5 | large?: boolean; 6 | onClick: () => void; 7 | disabled?: boolean; 8 | outline?: boolean; 9 | } 10 | 11 | const Button: React.FC = ({ 12 | label, 13 | secondary, 14 | fullWidth, 15 | onClick, 16 | large, 17 | disabled, 18 | outline 19 | }) => { 20 | return ( 21 | 46 | ); 47 | } 48 | 49 | export default Button; -------------------------------------------------------------------------------- /components/Form.tsx: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import { useCallback, useState } from 'react'; 3 | import { toast } from 'react-hot-toast'; 4 | 5 | import useLoginModal from '@/hooks/useLoginModal'; 6 | import useRegisterModal from '@/hooks/useRegisterModal'; 7 | import useCurrentUser from '@/hooks/useCurrentUser'; 8 | import usePosts from '@/hooks/usePosts'; 9 | import usePost from '@/hooks/usePost'; 10 | 11 | import Avatar from './Avatar'; 12 | import Button from './Button'; 13 | 14 | interface FormProps { 15 | placeholder: string; 16 | isComment?: boolean; 17 | postId?: string; 18 | } 19 | 20 | const Form: React.FC = ({ placeholder, isComment, postId }) => { 21 | const registerModal = useRegisterModal(); 22 | const loginModal = useLoginModal(); 23 | 24 | const { data: currentUser } = useCurrentUser(); 25 | const { mutate: mutatePosts } = usePosts(); 26 | const { mutate: mutatePost } = usePost(postId as string); 27 | 28 | const [body, setBody] = useState(''); 29 | const [isLoading, setIsLoading] = useState(false); 30 | 31 | const onSubmit = useCallback(async () => { 32 | try { 33 | setIsLoading(true); 34 | 35 | const url = isComment ? `/api/comments?postId=${postId}` : '/api/posts'; 36 | 37 | await axios.post(url, { body }); 38 | 39 | toast.success('Tweet created'); 40 | setBody(''); 41 | mutatePosts(); 42 | mutatePost(); 43 | } catch (error) { 44 | toast.error('Something went wrong'); 45 | } finally { 46 | setIsLoading(false); 47 | } 48 | }, [body, mutatePosts, isComment, postId, mutatePost]); 49 | 50 | return ( 51 |
52 | {currentUser ? ( 53 |
54 |
55 | 56 |
57 |
58 | 77 |
86 |
87 |
89 |
90 |
91 | ) : ( 92 |
93 |

Welcome to Twitter

94 |
95 |
98 |
99 | )} 100 |
101 | ); 102 | }; 103 | 104 | export default Form; 105 | -------------------------------------------------------------------------------- /components/Header.tsx: -------------------------------------------------------------------------------- 1 | import { useRouter } from "next/router"; 2 | import { useCallback } from "react"; 3 | import { BiArrowBack } from "react-icons/bi"; 4 | 5 | interface HeaderProps { 6 | showBackArrow?: boolean; 7 | label: string; 8 | } 9 | 10 | const Header: React.FC = ({showBackArrow, label }) => { 11 | const router = useRouter(); 12 | 13 | const handleBack = useCallback(() => { 14 | router.back(); 15 | }, [router]); 16 | 17 | return ( 18 |
19 |
20 | {showBackArrow && ( 21 | 30 | )} 31 |

32 | {label} 33 |

34 |
35 |
36 | ); 37 | } 38 | 39 | export default Header; 40 | -------------------------------------------------------------------------------- /components/ImageUpload.tsx: -------------------------------------------------------------------------------- 1 | import Image from "next/image"; 2 | import { useCallback, useState } from "react"; 3 | import { useDropzone } from "react-dropzone"; 4 | 5 | interface DropzoneProps { 6 | onChange: (base64: string) => void; 7 | label: string; 8 | value?: string; 9 | disabled?: boolean; 10 | } 11 | 12 | const ImageUpload: React.FC = ({ onChange, label, value, disabled }) => { 13 | const [base64, setBase64] = useState(value); 14 | 15 | const handleChange = useCallback((base64: string) => { 16 | onChange(base64); 17 | }, [onChange]); 18 | 19 | const handleDrop = useCallback((files: any) => { 20 | const file = files[0] 21 | const reader = new FileReader(); 22 | reader.onload = (event: any) => { 23 | setBase64(event.target.result); 24 | handleChange(event.target.result); 25 | }; 26 | reader.readAsDataURL(file); 27 | }, [handleChange]) 28 | 29 | const { getRootProps, getInputProps } = useDropzone({ 30 | maxFiles: 1, 31 | onDrop: handleDrop, 32 | disabled, 33 | accept: { 34 | 'image/jpeg': [], 35 | 'image/png': [], 36 | } 37 | }); 38 | 39 | return ( 40 |
41 | 42 | {base64 ? ( 43 |
44 | Uploaded image 50 |
51 | ) : ( 52 |

{label}

53 | )} 54 |
55 | ); 56 | } 57 | 58 | export default ImageUpload; -------------------------------------------------------------------------------- /components/Input.tsx: -------------------------------------------------------------------------------- 1 | interface InputProps { 2 | placeholder?: string; 3 | value?: string; 4 | type?: string; 5 | disabled?: boolean; 6 | onChange: (event: React.ChangeEvent) => void; 7 | label?: string; 8 | } 9 | 10 | const Input: React.FC = ({ placeholder, value, type = "text", onChange, disabled, label }) => { 11 | return ( 12 |
13 | {label &&

{label}

} 14 | 38 |
39 | ); 40 | } 41 | 42 | export default Input; -------------------------------------------------------------------------------- /components/Layout.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import FollowBar from "@/components/layout/FollowBar" 4 | import Sidebar from "@/components/layout/Sidebar" 5 | 6 | const Layout: React.FC<{ children: React.ReactNode }> = ({ children }) => { 7 | return ( 8 |
9 |
10 |
11 | 12 |
19 | {children} 20 |
21 | 22 |
23 |
24 |
25 | ) 26 | } 27 | 28 | export default Layout; 29 | -------------------------------------------------------------------------------- /components/Modal.tsx: -------------------------------------------------------------------------------- 1 | import { useCallback } from "react"; 2 | import { AiOutlineClose } from "react-icons/ai"; 3 | import Button from "./Button"; 4 | 5 | interface ModalProps { 6 | isOpen?: boolean; 7 | onClose: () => void; 8 | onSubmit: () => void; 9 | title?: string; 10 | body?: React.ReactElement; 11 | footer?: React.ReactElement; 12 | actionLabel: string; 13 | disabled?: boolean; 14 | } 15 | 16 | const Modal: React.FC = ({ isOpen, onClose, onSubmit, title, body, actionLabel, footer, disabled }) => { 17 | const handleClose = useCallback(() => { 18 | if (disabled) { 19 | return; 20 | } 21 | 22 | onClose(); 23 | }, [onClose, disabled]); 24 | 25 | const handleSubmit = useCallback(() => { 26 | if (disabled) { 27 | return; 28 | } 29 | 30 | onSubmit(); 31 | }, [onSubmit, disabled]); 32 | 33 | if (!isOpen) { 34 | return null; 35 | } 36 | 37 | return ( 38 | <> 39 |
55 |
56 | {/*content*/} 57 |
72 | {/*header*/} 73 |
81 |

82 | {title} 83 |

84 | 97 |
98 | {/*body*/} 99 |
100 | {body} 101 |
102 | {/*footer*/} 103 |
104 |
107 |
108 |
109 |
110 | 111 | ); 112 | } 113 | 114 | export default Modal; 115 | -------------------------------------------------------------------------------- /components/NotificationsFeed.tsx: -------------------------------------------------------------------------------- 1 | import { BsTwitter } from "react-icons/bs"; 2 | 3 | import useNotifications from "@/hooks/useNotifications"; 4 | import useCurrentUser from "@/hooks/useCurrentUser"; 5 | import { useEffect } from "react"; 6 | 7 | const NotificationsFeed = () => { 8 | const { data: currentUser, mutate: mutateCurrentUser } = useCurrentUser(); 9 | const { data: fetchedNotifications = [] } = useNotifications(currentUser?.id); 10 | 11 | useEffect(() => { 12 | mutateCurrentUser(); 13 | }, [mutateCurrentUser]); 14 | 15 | if (fetchedNotifications.length === 0) { 16 | return ( 17 |
18 | No notifications 19 |
20 | ) 21 | } 22 | 23 | return ( 24 |
25 | {fetchedNotifications.map((notification: Record) => ( 26 |
27 | 28 |

29 | {notification.body} 30 |

31 |
32 | ))} 33 |
34 | ); 35 | } 36 | 37 | export default NotificationsFeed; -------------------------------------------------------------------------------- /components/layout/FollowBar.tsx: -------------------------------------------------------------------------------- 1 | import useUsers from '@/hooks/useUsers'; 2 | 3 | import Avatar from '../Avatar'; 4 | 5 | const FollowBar = () => { 6 | const { data: users = [] } = useUsers(); 7 | 8 | if (users.length === 0) { 9 | return null; 10 | } 11 | 12 | return ( 13 |
14 |
15 |

Who to follow

16 |
17 | {users.map((user: Record) => ( 18 |
19 | 20 |
21 |

{user.name}

22 |

@{user.username}

23 |
24 |
25 | ))} 26 |
27 |
28 |
29 | ); 30 | }; 31 | 32 | export default FollowBar; 33 | -------------------------------------------------------------------------------- /components/layout/Sidebar.tsx: -------------------------------------------------------------------------------- 1 | import { signOut } from 'next-auth/react'; 2 | import { BiLogOut } from 'react-icons/bi'; 3 | import { BsHouseFill, BsBellFill } from 'react-icons/bs'; 4 | import { FaUser } from 'react-icons/fa'; 5 | 6 | import useCurrentUser from '@/hooks/useCurrentUser'; 7 | 8 | import SidebarItem from './SidebarItem'; 9 | import SidebarLogo from './SidebarLogo'; 10 | import SidebarTweetButton from './SidebarTweetButton'; 11 | 12 | const Sidebar = () => { 13 | const { data: currentUser } = useCurrentUser(); 14 | 15 | const items = [ 16 | { 17 | icon: BsHouseFill, 18 | label: 'Home', 19 | href: '/', 20 | }, 21 | { 22 | icon: BsBellFill, 23 | label: 'Notifications', 24 | href: '/notifications', 25 | auth: true, 26 | alert: currentUser?.hasNotification 27 | }, 28 | { 29 | icon: FaUser, 30 | label: 'Profile', 31 | href: `/users/${currentUser?.id}`, 32 | auth: true, 33 | }, 34 | ] 35 | 36 | return ( 37 |
38 |
39 |
40 | 41 | {items.map((item) => ( 42 | 50 | ))} 51 | {currentUser && signOut()} icon={BiLogOut} label="Logout" />} 52 | 53 |
54 |
55 |
56 | ) 57 | }; 58 | 59 | export default Sidebar; 60 | -------------------------------------------------------------------------------- /components/layout/SidebarItem.tsx: -------------------------------------------------------------------------------- 1 | import React, { useCallback } from 'react'; 2 | import { IconType } from "react-icons"; 3 | import { useRouter } from 'next/router'; 4 | 5 | import useLoginModal from '@/hooks/useLoginModal'; 6 | import useCurrentUser from '@/hooks/useCurrentUser'; 7 | import { BsDot } from 'react-icons/bs'; 8 | 9 | interface SidebarItemProps { 10 | label: string; 11 | icon: IconType; 12 | href?: string; 13 | onClick?: () => void; 14 | auth?: boolean; 15 | alert?: boolean; 16 | } 17 | 18 | const SidebarItem: React.FC = ({ label, icon: Icon, href, auth, onClick, alert }) => { 19 | const router = useRouter(); 20 | const loginModal = useLoginModal(); 21 | 22 | const { data: currentUser } = useCurrentUser(); 23 | 24 | const handleClick = useCallback(() => { 25 | if (onClick) { 26 | return onClick(); 27 | } 28 | 29 | if (auth && !currentUser) { 30 | loginModal.onOpen(); 31 | } else if (href) { 32 | router.push(href); 33 | } 34 | }, [router, href, auth, loginModal, onClick, currentUser]); 35 | 36 | return ( 37 |
38 |
52 | 53 | {alert ? : null} 54 |
55 |
68 | 69 |

70 | {label} 71 |

72 | {alert ? : null} 73 |
74 |
75 | ); 76 | } 77 | 78 | export default SidebarItem; -------------------------------------------------------------------------------- /components/layout/SidebarLogo.tsx: -------------------------------------------------------------------------------- 1 | import { useRouter } from "next/router"; 2 | import { BsTwitter } from "react-icons/bs"; 3 | 4 | const SidebarLogo = () => { 5 | const router = useRouter(); 6 | 7 | return ( 8 |
router.push('/')} 10 | className=" 11 | rounded-full 12 | h-14 13 | w-14 14 | p-4 15 | flex 16 | items-center 17 | justify-center 18 | hover:bg-blue-300 19 | hover:bg-opacity-10 20 | cursor-pointer 21 | "> 22 | 23 |
24 | ); 25 | }; 26 | 27 | export default SidebarLogo; 28 | -------------------------------------------------------------------------------- /components/layout/SidebarTweetButton.tsx: -------------------------------------------------------------------------------- 1 | import { useCallback } from "react"; 2 | import { FaFeather } from "react-icons/fa"; 3 | import { useRouter } from "next/router"; 4 | 5 | import useLoginModal from "@/hooks/useLoginModal"; 6 | import useCurrentUser from "@/hooks/useCurrentUser"; 7 | 8 | const SidebarTweetButton = () => { 9 | const router = useRouter(); 10 | const loginModal = useLoginModal(); 11 | const { data: currentUser } = useCurrentUser(); 12 | 13 | const onClick = useCallback(() => { 14 | if (!currentUser) { 15 | return loginModal.onOpen(); 16 | } 17 | 18 | router.push('/'); 19 | }, [loginModal, router, currentUser]); 20 | 21 | return ( 22 |
23 |
38 | 39 |
40 |
51 |

60 | Tweet 61 |

62 |
63 |
64 | ); 65 | }; 66 | 67 | export default SidebarTweetButton; 68 | -------------------------------------------------------------------------------- /components/modals/EditModal.tsx: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | import { useCallback, useEffect, useState } from "react"; 3 | import { toast } from "react-hot-toast"; 4 | 5 | import useCurrentUser from "@/hooks/useCurrentUser"; 6 | import useEditModal from "@/hooks/useEditModal"; 7 | import useUser from "@/hooks/useUser"; 8 | 9 | import Input from "../Input"; 10 | import Modal from "../Modal"; 11 | import ImageUpload from "../ImageUpload"; 12 | 13 | const EditModal = () => { 14 | const { data: currentUser } = useCurrentUser(); 15 | const { mutate: mutateFetchedUser } = useUser(currentUser?.id); 16 | const editModal = useEditModal(); 17 | 18 | const [profileImage, setProfileImage] = useState(''); 19 | const [coverImage, setCoverImage] = useState(''); 20 | const [name, setName] = useState(''); 21 | const [username, setUsername] = useState(''); 22 | const [bio, setBio] = useState(''); 23 | 24 | useEffect(() => { 25 | setProfileImage(currentUser?.profileImage) 26 | setCoverImage(currentUser?.coverImage) 27 | setName(currentUser?.name) 28 | setUsername(currentUser?.username) 29 | setBio(currentUser?.bio) 30 | }, [currentUser?.name, currentUser?.username, currentUser?.bio, currentUser?.profileImage, currentUser?.coverImage]); 31 | 32 | const [isLoading, setIsLoading] = useState(false); 33 | 34 | const onSubmit = useCallback(async () => { 35 | try { 36 | setIsLoading(true); 37 | 38 | await axios.patch('/api/edit', { name, username, bio, profileImage, coverImage }); 39 | mutateFetchedUser(); 40 | 41 | toast.success('Updated'); 42 | 43 | editModal.onClose(); 44 | } catch (error) { 45 | toast.error('Something went wrong'); 46 | } finally { 47 | setIsLoading(false); 48 | } 49 | }, [editModal, name, username, bio, mutateFetchedUser, profileImage, coverImage]); 50 | 51 | const bodyContent = ( 52 |
53 | setProfileImage(image)} label="Upload profile image" /> 54 | setCoverImage(image)} label="Upload cover image" /> 55 | setName(e.target.value)} 58 | value={name} 59 | disabled={isLoading} 60 | /> 61 | setUsername(e.target.value)} 64 | value={username} 65 | disabled={isLoading} 66 | /> 67 | setBio(e.target.value)} 70 | value={bio} 71 | disabled={isLoading} 72 | /> 73 |
74 | ) 75 | 76 | return ( 77 | 86 | ); 87 | } 88 | 89 | export default EditModal; 90 | -------------------------------------------------------------------------------- /components/modals/LoginModal.tsx: -------------------------------------------------------------------------------- 1 | import { signIn } from "next-auth/react"; 2 | import { useCallback, useState } from "react"; 3 | import { toast } from "react-hot-toast"; 4 | 5 | import useLoginModal from "@/hooks/useLoginModal"; 6 | import useRegisterModal from "@/hooks/useRegisterModal"; 7 | 8 | import Input from "../Input"; 9 | import Modal from "../Modal"; 10 | 11 | const LoginModal = () => { 12 | const loginModal = useLoginModal(); 13 | const registerModal = useRegisterModal(); 14 | 15 | const [email, setEmail] = useState(''); 16 | const [password, setPassword] = useState(''); 17 | const [isLoading, setIsLoading] = useState(false); 18 | 19 | const onSubmit = useCallback(async () => { 20 | try { 21 | setIsLoading(true); 22 | 23 | await signIn('credentials', { 24 | email, 25 | password, 26 | }); 27 | 28 | toast.success('Logged in'); 29 | 30 | loginModal.onClose(); 31 | } catch (error) { 32 | toast.error('Something went wrong'); 33 | } finally { 34 | setIsLoading(false); 35 | } 36 | }, [email, password, loginModal]); 37 | 38 | const onToggle = useCallback(() => { 39 | loginModal.onClose(); 40 | registerModal.onOpen(); 41 | }, [loginModal, registerModal]) 42 | 43 | const bodyContent = ( 44 |
45 | setEmail(e.target.value)} 48 | value={email} 49 | disabled={isLoading} 50 | /> 51 | setPassword(e.target.value)} 55 | value={password} 56 | disabled={isLoading} 57 | /> 58 |
59 | ) 60 | 61 | const footerContent = ( 62 |
63 |

First time using Twitter? 64 | Create an account 72 |

73 |
74 | ) 75 | 76 | return ( 77 | 87 | ); 88 | } 89 | 90 | export default LoginModal; 91 | -------------------------------------------------------------------------------- /components/modals/RegisterModal.tsx: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | import { toast } from "react-hot-toast"; 3 | import { useCallback, useState } from "react"; 4 | import { signIn } from 'next-auth/react'; 5 | 6 | import useLoginModal from "@/hooks/useLoginModal"; 7 | import useRegisterModal from "@/hooks/useRegisterModal"; 8 | 9 | import Input from "../Input"; 10 | import Modal from "../Modal"; 11 | 12 | const RegisterModal = () => { 13 | const loginModal = useLoginModal(); 14 | const registerModal = useRegisterModal(); 15 | 16 | const [email, setEmail] = useState(''); 17 | const [password, setPassword] = useState(''); 18 | const [username, setUsername] = useState(''); 19 | const [name, setName] = useState(''); 20 | 21 | const [isLoading, setIsLoading] = useState(false); 22 | 23 | const onToggle = useCallback(() => { 24 | if (isLoading) { 25 | return; 26 | } 27 | 28 | registerModal.onClose(); 29 | loginModal.onOpen(); 30 | }, [loginModal, registerModal, isLoading]); 31 | 32 | const onSubmit = useCallback(async () => { 33 | try { 34 | setIsLoading(true); 35 | 36 | await axios.post('/api/register', { 37 | email, 38 | password, 39 | username, 40 | name, 41 | }); 42 | 43 | setIsLoading(false) 44 | 45 | toast.success('Account created.'); 46 | 47 | signIn('credentials', { 48 | email, 49 | password, 50 | }); 51 | 52 | registerModal.onClose(); 53 | } catch (error) { 54 | toast.error('Something went wrong'); 55 | } finally { 56 | setIsLoading(false); 57 | } 58 | }, [email, password, registerModal, username, name]); 59 | 60 | const bodyContent = ( 61 |
62 | setEmail(e.target.value)} 67 | /> 68 | setName(e.target.value)} 73 | /> 74 | setUsername(e.target.value)} 79 | /> 80 | setPassword(e.target.value)} 86 | /> 87 |
88 | ) 89 | 90 | const footerContent = ( 91 |
92 |

Already have an account? 93 | Sign in 101 |

102 |
103 | ) 104 | 105 | return ( 106 | 116 | ); 117 | } 118 | 119 | export default RegisterModal; 120 | -------------------------------------------------------------------------------- /components/posts/CommentFeed.tsx: -------------------------------------------------------------------------------- 1 | import CommentItem from './CommentItem'; 2 | 3 | interface CommentFeedProps { 4 | comments?: Record[]; 5 | } 6 | 7 | const CommentFeed: React.FC = ({ comments = [] }) => { 8 | return ( 9 | <> 10 | {comments.map((comment: Record,) => ( 11 | 12 | ))} 13 | 14 | ); 15 | }; 16 | 17 | export default CommentFeed; 18 | -------------------------------------------------------------------------------- /components/posts/CommentItem.tsx: -------------------------------------------------------------------------------- 1 | import { useRouter } from 'next/router'; 2 | import { useCallback, useMemo } from 'react'; 3 | import { formatDistanceToNowStrict } from 'date-fns'; 4 | 5 | import Avatar from '../Avatar'; 6 | 7 | interface CommentItemProps { 8 | data: Record; 9 | } 10 | 11 | const CommentItem: React.FC = ({ data = {} }) => { 12 | const router = useRouter(); 13 | 14 | const goToUser = useCallback((ev: any) => { 15 | ev.stopPropagation(); 16 | 17 | router.push(`/users/${data.user.id}`) 18 | }, [router, data.user.id]); 19 | 20 | const createdAt = useMemo(() => { 21 | if (!data?.createdAt) { 22 | return null; 23 | } 24 | 25 | return formatDistanceToNowStrict(new Date(data.createdAt)); 26 | }, [data.createdAt]) 27 | 28 | return ( 29 |
38 |
39 | 40 |
41 |
42 |

50 | {data.user.name} 51 |

52 | 61 | @{data.user.username} 62 | 63 | 64 | {createdAt} 65 | 66 |
67 |
68 | {data.body} 69 |
70 |
71 |
72 |
73 | ) 74 | } 75 | 76 | export default CommentItem; 77 | -------------------------------------------------------------------------------- /components/posts/PostFeed.tsx: -------------------------------------------------------------------------------- 1 | import usePosts from '@/hooks/usePosts'; 2 | 3 | import PostItem from './PostItem'; 4 | 5 | interface PostFeedProps { 6 | userId?: string; 7 | } 8 | 9 | const PostFeed: React.FC = ({ userId }) => { 10 | const { data: posts = [] } = usePosts(userId); 11 | 12 | return ( 13 | <> 14 | {posts.map((post: Record,) => ( 15 | 16 | ))} 17 | 18 | ); 19 | }; 20 | 21 | export default PostFeed; 22 | -------------------------------------------------------------------------------- /components/posts/PostItem.tsx: -------------------------------------------------------------------------------- 1 | import { useRouter } from 'next/router'; 2 | import { useCallback, useMemo } from 'react'; 3 | import { AiFillHeart, AiOutlineHeart, AiOutlineMessage } from 'react-icons/ai'; 4 | import { formatDistanceToNowStrict } from 'date-fns'; 5 | 6 | import useLoginModal from '@/hooks/useLoginModal'; 7 | import useCurrentUser from '@/hooks/useCurrentUser'; 8 | import useLike from '@/hooks/useLike'; 9 | 10 | import Avatar from '../Avatar'; 11 | interface PostItemProps { 12 | data: Record; 13 | userId?: string; 14 | } 15 | 16 | const PostItem: React.FC = ({ data = {}, userId }) => { 17 | const router = useRouter(); 18 | const loginModal = useLoginModal(); 19 | 20 | const { data: currentUser } = useCurrentUser(); 21 | const { hasLiked, toggleLike } = useLike({ postId: data.id, userId}); 22 | 23 | const goToUser = useCallback((ev: any) => { 24 | ev.stopPropagation(); 25 | router.push(`/users/${data.user.id}`) 26 | }, [router, data.user.id]); 27 | 28 | const goToPost = useCallback(() => { 29 | router.push(`/posts/${data.id}`); 30 | }, [router, data.id]); 31 | 32 | const onLike = useCallback(async (ev: any) => { 33 | ev.stopPropagation(); 34 | 35 | if (!currentUser) { 36 | return loginModal.onOpen(); 37 | } 38 | 39 | toggleLike(); 40 | }, [loginModal, currentUser, toggleLike]); 41 | 42 | const LikeIcon = hasLiked ? AiFillHeart : AiOutlineHeart; 43 | 44 | const createdAt = useMemo(() => { 45 | if (!data?.createdAt) { 46 | return null; 47 | } 48 | 49 | return formatDistanceToNowStrict(new Date(data.createdAt)); 50 | }, [data.createdAt]) 51 | 52 | return ( 53 |
63 |
64 | 65 |
66 |
67 |

75 | {data.user.name} 76 |

77 | 86 | @{data.user.username} 87 | 88 | 89 | {createdAt} 90 | 91 |
92 |
93 | {data.body} 94 |
95 |
96 |
107 | 108 |

109 | {data.comments?.length || 0} 110 |

111 |
112 |
124 | 125 |

126 | {data.likedIds.length} 127 |

128 |
129 |
130 |
131 |
132 |
133 | ) 134 | } 135 | 136 | export default PostItem; 137 | -------------------------------------------------------------------------------- /components/users/UserBio.tsx: -------------------------------------------------------------------------------- 1 | import { useMemo } from "react"; 2 | import { BiCalendar } from "react-icons/bi"; 3 | import { format } from "date-fns"; 4 | 5 | import useCurrentUser from "@/hooks/useCurrentUser"; 6 | import useUser from "@/hooks/useUser"; 7 | import useFollow from "@/hooks/useFollow"; 8 | import useEditModal from "@/hooks/useEditModal"; 9 | 10 | import Button from "../Button"; 11 | 12 | interface UserBioProps { 13 | userId: string; 14 | } 15 | 16 | const UserBio: React.FC = ({ userId }) => { 17 | const { data: currentUser } = useCurrentUser(); 18 | const { data: fetchedUser } = useUser(userId); 19 | 20 | const editModal = useEditModal(); 21 | 22 | const { isFollowing, toggleFollow } = useFollow(userId); 23 | 24 | const createdAt = useMemo(() => { 25 | if (!fetchedUser?.createdAt) { 26 | return null; 27 | } 28 | 29 | return format(new Date(fetchedUser.createdAt), 'MMMM yyyy'); 30 | }, [fetchedUser?.createdAt]) 31 | 32 | 33 | return ( 34 |
35 |
36 | {currentUser?.id === userId ? ( 37 |
47 |
48 |
49 |

50 | {fetchedUser?.name} 51 |

52 |

53 | @{fetchedUser?.username} 54 |

55 |
56 |
57 |

58 | {fetchedUser?.bio} 59 |

60 |
69 | 70 |

71 | Joined {createdAt} 72 |

73 |
74 |
75 |
76 |
77 |

{fetchedUser?.followingIds?.length}

78 |

Following

79 |
80 |
81 |

{fetchedUser?.followersCount || 0}

82 |

Followers

83 |
84 |
85 |
86 |
87 | ); 88 | } 89 | 90 | export default UserBio; -------------------------------------------------------------------------------- /components/users/UserHero.tsx: -------------------------------------------------------------------------------- 1 | import Image from "next/image"; 2 | 3 | import useUser from "@/hooks/useUser"; 4 | 5 | import Avatar from "../Avatar" 6 | 7 | interface UserHeroProps { 8 | userId: string; 9 | } 10 | 11 | const UserHero: React.FC = ({ userId }) => { 12 | const { data: fetchedUser } = useUser(userId); 13 | 14 | return ( 15 |
16 |
17 | {fetchedUser?.coverImage && ( 18 | Cover Image 19 | )} 20 |
21 | 22 |
23 |
24 |
25 | ); 26 | } 27 | 28 | export default UserHero; -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '2' 2 | services: 3 | mongodb-primary: 4 | image: 'bitnami/mongodb:latest' 5 | environment: 6 | - MONGODB_ADVERTISED_HOSTNAME=mongodb-primary 7 | - MONGODB_REPLICA_SET_MODE=primary 8 | - MONGODB_ROOT_PASSWORD=prisma 9 | - MONGODB_REPLICA_SET_KEY=replicasetkey123 10 | ports: 11 | - 27017:27017 12 | 13 | volumes: 14 | - 'mongodb_master_data:/bitnami' 15 | 16 | mongodb-secondary: 17 | image: 'bitnami/mongodb:latest' 18 | depends_on: 19 | - mongodb-primary 20 | environment: 21 | - MONGODB_ADVERTISED_HOSTNAME=mongodb-secondary 22 | - MONGODB_REPLICA_SET_MODE=secondary 23 | - MONGODB_INITIAL_PRIMARY_HOST=mongodb-primary 24 | - MONGODB_INITIAL_PRIMARY_PORT_NUMBER=27017 25 | - MONGODB_INITIAL_PRIMARY_ROOT_PASSWORD=prisma 26 | - MONGODB_REPLICA_SET_KEY=replicasetkey123 27 | ports: 28 | - 27027:27017 29 | 30 | mongodb-arbiter: 31 | image: 'bitnami/mongodb:latest' 32 | depends_on: 33 | - mongodb-primary 34 | environment: 35 | - MONGODB_ADVERTISED_HOSTNAME=mongodb-arbiter 36 | - MONGODB_REPLICA_SET_MODE=arbiter 37 | - MONGODB_INITIAL_PRIMARY_HOST=mongodb-primary 38 | - MONGODB_INITIAL_PRIMARY_PORT_NUMBER=27017 39 | - MONGODB_INITIAL_PRIMARY_ROOT_PASSWORD=prisma 40 | - MONGODB_REPLICA_SET_KEY=replicasetkey123 41 | ports: 42 | - 27037:27017 43 | 44 | nxtjs-frontend: 45 | build: 46 | context: . 47 | dockerfile: Dockerfile 48 | restart: always 49 | ports: 50 | - "3000:3000" 51 | depends_on: 52 | - mongodb-primary 53 | - mongodb-secondary 54 | - mongodb-arbiter 55 | 56 | volumes: 57 | mongodb_master_data: 58 | driver: local 59 | 60 | -------------------------------------------------------------------------------- /docker-compose.yml-og: -------------------------------------------------------------------------------- 1 | version: '2' 2 | services: 3 | twitter: 4 | build: 5 | context: . 6 | dockerfile: Dockerfile 7 | container_name: twitter-app 8 | restart: always 9 | ports: 10 | - "3000:3000" 11 | 12 | mongodb-primary: 13 | image: 'bitnami/mongodb:latest' 14 | environment: 15 | - MONGODB_ADVERTISED_HOSTNAME=mongodb-primary 16 | - MONGODB_REPLICA_SET_MODE=primary 17 | - MONGODB_ROOT_PASSWORD=prisma 18 | - MONGODB_REPLICA_SET_KEY=replicasetkey123 19 | ports: 20 | - 27017:27017 21 | 22 | volumes: 23 | - 'mongodb_master_data:/bitnami' 24 | 25 | mongodb-secondary: 26 | image: 'bitnami/mongodb:latest' 27 | depends_on: 28 | - mongodb-primary 29 | environment: 30 | - MONGODB_ADVERTISED_HOSTNAME=mongodb-secondary 31 | - MONGODB_REPLICA_SET_MODE=secondary 32 | - MONGODB_INITIAL_PRIMARY_HOST=mongodb-primary 33 | - MONGODB_INITIAL_PRIMARY_PORT_NUMBER=27017 34 | - MONGODB_INITIAL_PRIMARY_ROOT_PASSWORD=prisma 35 | - MONGODB_REPLICA_SET_KEY=replicasetkey123 36 | ports: 37 | - 27027:27017 38 | 39 | mongodb-arbiter: 40 | image: 'bitnami/mongodb:latest' 41 | depends_on: 42 | - mongodb-primary 43 | environment: 44 | - MONGODB_ADVERTISED_HOSTNAME=mongodb-arbiter 45 | - MONGODB_REPLICA_SET_MODE=arbiter 46 | - MONGODB_INITIAL_PRIMARY_HOST=mongodb-primary 47 | - MONGODB_INITIAL_PRIMARY_PORT_NUMBER=27017 48 | - MONGODB_INITIAL_PRIMARY_ROOT_PASSWORD=prisma 49 | - MONGODB_REPLICA_SET_KEY=replicasetkey123 50 | ports: 51 | - 27037:27017 52 | 53 | volumes: 54 | mongodb_master_data: 55 | driver: local 56 | 57 | -------------------------------------------------------------------------------- /hooks/useCurrentUser.ts: -------------------------------------------------------------------------------- 1 | import useSWR from 'swr'; 2 | 3 | import fetcher from '@/libs/fetcher'; 4 | 5 | const useCurrentUser = () => { 6 | const { data, error, isLoading, mutate } = useSWR('/api/current', fetcher); 7 | 8 | return { 9 | data, 10 | error, 11 | isLoading, 12 | mutate 13 | } 14 | }; 15 | 16 | export default useCurrentUser; 17 | -------------------------------------------------------------------------------- /hooks/useEditModal.ts: -------------------------------------------------------------------------------- 1 | import { create } from 'zustand'; 2 | 3 | interface EditModalStore { 4 | isOpen: boolean; 5 | onOpen: () => void; 6 | onClose: () => void; 7 | } 8 | 9 | const useEditModal = create((set) => ({ 10 | isOpen: false, 11 | onOpen: () => set({ isOpen: true }), 12 | onClose: () => set({ isOpen: false }) 13 | })); 14 | 15 | 16 | export default useEditModal; 17 | -------------------------------------------------------------------------------- /hooks/useFollow.ts: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | import { useCallback, useMemo } from "react"; 3 | import { toast } from "react-hot-toast"; 4 | 5 | import useCurrentUser from "./useCurrentUser"; 6 | import useLoginModal from "./useLoginModal"; 7 | import useUser from "./useUser"; 8 | 9 | const useFollow = (userId: string) => { 10 | const { data: currentUser, mutate: mutateCurrentUser } = useCurrentUser(); 11 | const { mutate: mutateFetchedUser } = useUser(userId); 12 | 13 | const loginModal = useLoginModal(); 14 | 15 | const isFollowing = useMemo(() => { 16 | const list = currentUser?.followingIds || []; 17 | 18 | return list.includes(userId); 19 | }, [currentUser, userId]); 20 | 21 | const toggleFollow = useCallback(async () => { 22 | if (!currentUser) { 23 | return loginModal.onOpen(); 24 | } 25 | 26 | try { 27 | let request; 28 | 29 | if (isFollowing) { 30 | request = () => axios.delete('/api/follow', { data: { userId } }); 31 | } else { 32 | request = () => axios.post('/api/follow', { userId }); 33 | } 34 | 35 | await request(); 36 | mutateCurrentUser(); 37 | mutateFetchedUser(); 38 | 39 | toast.success('Success'); 40 | } catch (error) { 41 | toast.error('Something went wrong'); 42 | } 43 | }, [currentUser, isFollowing, userId, mutateCurrentUser, mutateFetchedUser, loginModal]); 44 | 45 | return { 46 | isFollowing, 47 | toggleFollow, 48 | } 49 | } 50 | 51 | export default useFollow; 52 | -------------------------------------------------------------------------------- /hooks/useLike.ts: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | import { useCallback, useMemo } from "react"; 3 | import { toast } from "react-hot-toast"; 4 | 5 | import useCurrentUser from "./useCurrentUser"; 6 | import useLoginModal from "./useLoginModal"; 7 | import usePost from "./usePost"; 8 | import usePosts from "./usePosts"; 9 | 10 | const useLike = ({ postId, userId }: { postId: string, userId?: string }) => { 11 | const { data: currentUser } = useCurrentUser(); 12 | const { data: fetchedPost, mutate: mutateFetchedPost } = usePost(postId); 13 | const { mutate: mutateFetchedPosts } = usePosts(userId); 14 | 15 | const loginModal = useLoginModal(); 16 | 17 | const hasLiked = useMemo(() => { 18 | const list = fetchedPost?.likedIds || []; 19 | 20 | return list.includes(currentUser?.id); 21 | }, [fetchedPost, currentUser]); 22 | 23 | const toggleLike = useCallback(async () => { 24 | if (!currentUser) { 25 | return loginModal.onOpen(); 26 | } 27 | 28 | try { 29 | let request; 30 | 31 | if (hasLiked) { 32 | request = () => axios.delete('/api/like', { data: { postId } }); 33 | } else { 34 | request = () => axios.post('/api/like', { postId }); 35 | } 36 | 37 | await request(); 38 | mutateFetchedPost(); 39 | mutateFetchedPosts(); 40 | 41 | toast.success('Success'); 42 | } catch (error) { 43 | toast.error('Something went wrong'); 44 | } 45 | }, [currentUser, hasLiked, postId, mutateFetchedPosts, mutateFetchedPost, loginModal]); 46 | 47 | return { 48 | hasLiked, 49 | toggleLike, 50 | } 51 | } 52 | 53 | export default useLike; 54 | -------------------------------------------------------------------------------- /hooks/useLoginModal.ts: -------------------------------------------------------------------------------- 1 | import { create } from 'zustand'; 2 | 3 | interface LoginModalStore { 4 | isOpen: boolean; 5 | onOpen: () => void; 6 | onClose: () => void; 7 | } 8 | 9 | const useLoginModal = create((set) => ({ 10 | isOpen: false, 11 | onOpen: () => set({ isOpen: true }), 12 | onClose: () => set({ isOpen: false }) 13 | })); 14 | 15 | 16 | export default useLoginModal; 17 | -------------------------------------------------------------------------------- /hooks/useNotifications.ts: -------------------------------------------------------------------------------- 1 | import useSWR from 'swr'; 2 | 3 | import fetcher from '@/libs/fetcher'; 4 | 5 | const useNotifications = (userId?: string) => { 6 | const url = userId ? `/api/notifications/${userId}` : null; 7 | const { data, error, isLoading, mutate } = useSWR(url, fetcher); 8 | 9 | return { 10 | data, 11 | error, 12 | isLoading, 13 | mutate 14 | } 15 | }; 16 | 17 | export default useNotifications; 18 | -------------------------------------------------------------------------------- /hooks/usePost.ts: -------------------------------------------------------------------------------- 1 | import useSWR from 'swr'; 2 | 3 | import fetcher from '@/libs/fetcher'; 4 | 5 | const usePost = (postId: string) => { 6 | const { data, error, isLoading, mutate } = useSWR(postId ? `/api/posts/${postId}` : null, fetcher); 7 | 8 | return { 9 | data, 10 | error, 11 | isLoading, 12 | mutate 13 | } 14 | }; 15 | 16 | export default usePost; 17 | -------------------------------------------------------------------------------- /hooks/usePosts.ts: -------------------------------------------------------------------------------- 1 | import useSWR from 'swr'; 2 | 3 | import fetcher from '@/libs/fetcher'; 4 | 5 | const usePosts = (userId?: string) => { 6 | const url = userId ? `/api/posts?userId=${userId}` : '/api/posts'; 7 | const { data, error, isLoading, mutate } = useSWR(url, fetcher); 8 | 9 | return { 10 | data, 11 | error, 12 | isLoading, 13 | mutate 14 | } 15 | }; 16 | 17 | export default usePosts; 18 | -------------------------------------------------------------------------------- /hooks/useRegisterModal.ts: -------------------------------------------------------------------------------- 1 | import { create } from 'zustand'; 2 | 3 | interface RegisterModalStore { 4 | isOpen: boolean; 5 | onOpen: () => void; 6 | onClose: () => void; 7 | } 8 | 9 | const useRegisterModal = create((set) => ({ 10 | isOpen: false, 11 | onOpen: () => set({ isOpen: true }), 12 | onClose: () => set({ isOpen: false }) 13 | })); 14 | 15 | 16 | export default useRegisterModal; 17 | -------------------------------------------------------------------------------- /hooks/useUser.ts: -------------------------------------------------------------------------------- 1 | import useSWR from 'swr'; 2 | 3 | import fetcher from '@/libs/fetcher'; 4 | 5 | const useUser = (userId: string) => { 6 | const { data, error, isLoading, mutate } = useSWR(userId ? `/api/users/${userId}` : null, fetcher); 7 | 8 | return { 9 | data, 10 | error, 11 | isLoading, 12 | mutate 13 | } 14 | }; 15 | 16 | export default useUser; 17 | -------------------------------------------------------------------------------- /hooks/useUsers.ts: -------------------------------------------------------------------------------- 1 | import useSWR from 'swr'; 2 | 3 | import fetcher from '@/libs/fetcher'; 4 | 5 | const useUsers = () => { 6 | const { data, error, isLoading, mutate } = useSWR('/api/users', fetcher); 7 | 8 | return { 9 | data, 10 | error, 11 | isLoading, 12 | mutate 13 | } 14 | }; 15 | 16 | export default useUsers; 17 | -------------------------------------------------------------------------------- /libs/fetcher.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | 3 | const fetcher = (url: string) => axios.get(url).then((res) => res.data); 4 | 5 | export default fetcher; 6 | -------------------------------------------------------------------------------- /libs/prismadb.ts: -------------------------------------------------------------------------------- 1 | import { PrismaClient } from "@prisma/client" 2 | 3 | declare global { 4 | var prisma: PrismaClient | undefined 5 | } 6 | 7 | const client = globalThis.prisma || new PrismaClient() 8 | if (process.env.NODE_ENV !== "production") globalThis.prisma = client 9 | 10 | export default client -------------------------------------------------------------------------------- /libs/serverAuth.ts: -------------------------------------------------------------------------------- 1 | import { NextApiRequest } from 'next'; 2 | import { getSession } from 'next-auth/react'; 3 | 4 | import prisma from '@/libs/prismadb'; 5 | 6 | const serverAuth = async (req: NextApiRequest) => { 7 | const session = await getSession({ req }); 8 | 9 | if (!session?.user?.email) { 10 | throw new Error('Not signed in'); 11 | } 12 | 13 | const currentUser = await prisma.user.findUnique({ 14 | where: { 15 | email: session.user.email, 16 | } 17 | }); 18 | 19 | if (!currentUser) { 20 | throw new Error('Not signed in'); 21 | } 22 | 23 | return { currentUser }; 24 | }; 25 | 26 | export default serverAuth; 27 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | reactStrictMode: true, 4 | } 5 | 6 | module.exports = nextConfig 7 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "twitter-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 | }, 11 | "prisma": { 12 | "schema": "prisma/schema.prisma" 13 | }, 14 | "dependencies": { 15 | "@next-auth/prisma-adapter": "^1.0.5", 16 | "@prisma/client": "^4.11.0", 17 | "@types/lodash": "^4.14.191", 18 | "@types/node": "18.14.2", 19 | "@types/react": "18.0.28", 20 | "@types/react-dom": "18.0.11", 21 | "axios": "^1.3.4", 22 | "bcrypt": "^5.1.0", 23 | "date-fns": "^2.29.3", 24 | "eslint": "8.35.0", 25 | "eslint-config-next": "13.2.1", 26 | "lodash": "^4.17.21", 27 | "next": "13.2.1", 28 | "next-auth": "^4.20.1", 29 | "react": "18.2.0", 30 | "react-dom": "18.2.0", 31 | "react-dropzone": "^14.2.3", 32 | "react-hot-toast": "^2.4.0", 33 | "react-icons": "^4.7.1", 34 | "react-spinners": "^0.13.8", 35 | "react-toastify": "^9.1.1", 36 | "swr": "^2.0.4", 37 | "typescript": "4.9.5", 38 | "zustand": "^4.3.5" 39 | }, 40 | "devDependencies": { 41 | "@types/bcrypt": "^5.0.0", 42 | "autoprefixer": "^10.4.13", 43 | "postcss": "^8.4.21", 44 | "tailwindcss": "^3.2.7" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import type { AppProps } from 'next/app' 2 | import { Toaster } from 'react-hot-toast'; 3 | import { SessionProvider } from 'next-auth/react'; 4 | 5 | import Layout from '@/components/Layout' 6 | import LoginModal from '@/components/modals/LoginModal' 7 | import RegisterModal from '@/components/modals/RegisterModal' 8 | import '@/styles/globals.css' 9 | import EditModal from '@/components/modals/EditModal'; 10 | 11 | export default function App({ Component, pageProps }: AppProps) { 12 | return ( 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | ) 23 | } 24 | -------------------------------------------------------------------------------- /pages/api/auth/[...nextauth].ts: -------------------------------------------------------------------------------- 1 | import bcrypt from "bcrypt" 2 | import NextAuth from "next-auth" 3 | import CredentialsProvider from "next-auth/providers/credentials" 4 | import { PrismaAdapter } from "@next-auth/prisma-adapter" 5 | 6 | import prisma from "@/libs/prismadb" 7 | 8 | export default NextAuth({ 9 | adapter: PrismaAdapter(prisma), 10 | providers: [ 11 | CredentialsProvider({ 12 | name: 'credentials', 13 | credentials: { 14 | email: { label: 'email', type: 'text' }, 15 | password: { label: 'password', type: 'password' } 16 | }, 17 | async authorize(credentials) { 18 | if (!credentials?.email || !credentials?.password) { 19 | throw new Error('Invalid credentials'); 20 | } 21 | 22 | const user = await prisma.user.findUnique({ 23 | where: { 24 | email: credentials.email 25 | } 26 | }); 27 | 28 | if (!user || !user?.hashedPassword) { 29 | throw new Error('Invalid credentials'); 30 | } 31 | 32 | const isCorrectPassword = await bcrypt.compare( 33 | credentials.password, 34 | user.hashedPassword 35 | ); 36 | 37 | if (!isCorrectPassword) { 38 | throw new Error('Invalid credentials'); 39 | } 40 | 41 | return user; 42 | } 43 | }) 44 | ], 45 | debug: process.env.NODE_ENV === 'development', 46 | session: { 47 | strategy: 'jwt', 48 | }, 49 | jwt: { 50 | secret: process.env.NEXTAUTH_JWT_SECRET, 51 | }, 52 | secret: process.env.NEXTAUTH_SECRET, 53 | }); 54 | -------------------------------------------------------------------------------- /pages/api/comments.ts: -------------------------------------------------------------------------------- 1 | import { NextApiRequest, NextApiResponse } from "next"; 2 | 3 | import serverAuth from "@/libs/serverAuth"; 4 | import prisma from "@/libs/prismadb"; 5 | 6 | export default async function handler(req: NextApiRequest, res: NextApiResponse) { 7 | if (req.method !== 'POST') { 8 | return res.status(405).end(); 9 | } 10 | 11 | try { 12 | const { currentUser } = await serverAuth(req); 13 | const { body } = req.body; 14 | const { postId } = req.query; 15 | 16 | if (!postId || typeof postId !== 'string') { 17 | throw new Error('Invalid ID'); 18 | } 19 | 20 | const comment = await prisma.comment.create({ 21 | data: { 22 | body, 23 | userId: currentUser.id, 24 | postId 25 | } 26 | }); 27 | 28 | // NOTIFICATION PART START 29 | try { 30 | const post = await prisma.post.findUnique({ 31 | where: { 32 | id: postId, 33 | } 34 | }); 35 | 36 | if (post?.userId) { 37 | await prisma.notification.create({ 38 | data: { 39 | body: 'Someone replied on your tweet!', 40 | userId: post.userId 41 | } 42 | }); 43 | 44 | await prisma.user.update({ 45 | where: { 46 | id: post.userId 47 | }, 48 | data: { 49 | hasNotification: true 50 | } 51 | }); 52 | } 53 | } 54 | catch (error) { 55 | console.log(error); 56 | } 57 | // NOTIFICATION PART END 58 | 59 | return res.status(200).json(comment); 60 | } catch (error) { 61 | console.log(error); 62 | return res.status(400).end(); 63 | } 64 | } -------------------------------------------------------------------------------- /pages/api/current.ts: -------------------------------------------------------------------------------- 1 | import { NextApiRequest, NextApiResponse } from 'next'; 2 | 3 | import serverAuth from '@/libs/serverAuth'; 4 | 5 | export default async function handler(req: NextApiRequest, res: NextApiResponse) { 6 | if (req.method !== 'GET') { 7 | return res.status(405).end(); 8 | } 9 | 10 | try { 11 | const { currentUser } = await serverAuth(req); 12 | 13 | return res.status(200).json(currentUser); 14 | } catch (error) { 15 | console.log(error); 16 | return res.status(400).end(); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /pages/api/edit.ts: -------------------------------------------------------------------------------- 1 | import { NextApiRequest, NextApiResponse } from "next"; 2 | 3 | import serverAuth from "@/libs/serverAuth"; 4 | import prisma from "@/libs/prismadb"; 5 | 6 | export default async function handler(req: NextApiRequest, res: NextApiResponse) { 7 | if (req.method !== 'PATCH') { 8 | return res.status(405).end(); 9 | } 10 | 11 | try { 12 | const { currentUser } = await serverAuth(req); 13 | 14 | const { name, username, bio, profileImage, coverImage } = req.body; 15 | 16 | if (!name || !username) { 17 | throw new Error('Missing fields'); 18 | } 19 | 20 | const updatedUser = await prisma.user.update({ 21 | where: { 22 | id: currentUser.id, 23 | }, 24 | data: { 25 | name, 26 | username, 27 | bio, 28 | profileImage, 29 | coverImage 30 | } 31 | }); 32 | 33 | return res.status(200).json(updatedUser); 34 | } catch (error) { 35 | console.log(error); 36 | return res.status(400).end(); 37 | } 38 | } -------------------------------------------------------------------------------- /pages/api/follow.ts: -------------------------------------------------------------------------------- 1 | import { NextApiRequest, NextApiResponse } from "next"; 2 | 3 | import prisma from '@/libs/prismadb'; 4 | import serverAuth from "@/libs/serverAuth"; 5 | 6 | export default async function handler(req: NextApiRequest, res: NextApiResponse) { 7 | if (req.method !== 'POST' && req.method !== 'DELETE') { 8 | return res.status(405).end(); 9 | } 10 | 11 | try { 12 | const { userId } = req.body; 13 | 14 | const { currentUser } = await serverAuth(req); 15 | 16 | if (!userId || typeof userId !== 'string') { 17 | throw new Error('Invalid ID'); 18 | } 19 | 20 | const user = await prisma.user.findUnique({ 21 | where: { 22 | id: userId 23 | } 24 | }); 25 | 26 | if (!user) { 27 | throw new Error('Invalid ID'); 28 | } 29 | 30 | let updatedFollowingIds = [...(user.followingIds || [])]; 31 | 32 | if (req.method === 'POST') { 33 | updatedFollowingIds.push(userId); 34 | 35 | // NOTIFICATION PART START 36 | try { 37 | await prisma.notification.create({ 38 | data: { 39 | body: 'Someone followed you!', 40 | userId, 41 | }, 42 | }); 43 | 44 | await prisma.user.update({ 45 | where: { 46 | id: userId, 47 | }, 48 | data: { 49 | hasNotification: true, 50 | } 51 | }); 52 | } catch (error) { 53 | console.log(error); 54 | } 55 | // NOTIFICATION PART END 56 | 57 | } 58 | 59 | if (req.method === 'DELETE') { 60 | updatedFollowingIds = updatedFollowingIds.filter((followingId) => followingId !== userId); 61 | } 62 | 63 | const updatedUser = await prisma.user.update({ 64 | where: { 65 | id: currentUser.id 66 | }, 67 | data: { 68 | followingIds: updatedFollowingIds 69 | } 70 | }); 71 | 72 | return res.status(200).json(updatedUser); 73 | } catch (error) { 74 | console.log(error); 75 | return res.status(400).end(); 76 | } 77 | } -------------------------------------------------------------------------------- /pages/api/like.ts: -------------------------------------------------------------------------------- 1 | import { NextApiRequest, NextApiResponse } from "next"; 2 | 3 | import prisma from '@/libs/prismadb'; 4 | import serverAuth from "@/libs/serverAuth"; 5 | 6 | export default async function handler(req: NextApiRequest, res: NextApiResponse) { 7 | if (req.method !== 'POST' && req.method !== 'DELETE') { 8 | return res.status(405).end(); 9 | } 10 | 11 | try { 12 | const { postId } = req.body; 13 | 14 | const { currentUser } = await serverAuth(req); 15 | 16 | if (!postId || typeof postId !== 'string') { 17 | throw new Error('Invalid ID'); 18 | } 19 | 20 | const post = await prisma.post.findUnique({ 21 | where: { 22 | id: postId 23 | } 24 | }); 25 | 26 | if (!post) { 27 | throw new Error('Invalid ID'); 28 | } 29 | 30 | let updatedLikedIds = [...(post.likedIds || [])]; 31 | 32 | if (req.method === 'POST') { 33 | updatedLikedIds.push(currentUser.id); 34 | 35 | // NOTIFICATION PART START 36 | try { 37 | const post = await prisma.post.findUnique({ 38 | where: { 39 | id: postId, 40 | } 41 | }); 42 | 43 | if (post?.userId) { 44 | await prisma.notification.create({ 45 | data: { 46 | body: 'Someone liked your tweet!', 47 | userId: post.userId 48 | } 49 | }); 50 | 51 | await prisma.user.update({ 52 | where: { 53 | id: post.userId 54 | }, 55 | data: { 56 | hasNotification: true 57 | } 58 | }); 59 | } 60 | } catch(error) { 61 | console.log(error); 62 | } 63 | // NOTIFICATION PART END 64 | } 65 | 66 | if (req.method === 'DELETE') { 67 | updatedLikedIds = updatedLikedIds.filter((likedId) => likedId !== currentUser?.id); 68 | } 69 | 70 | const updatedPost = await prisma.post.update({ 71 | where: { 72 | id: postId 73 | }, 74 | data: { 75 | likedIds: updatedLikedIds 76 | } 77 | }); 78 | 79 | return res.status(200).json(updatedPost); 80 | } catch (error) { 81 | console.log(error); 82 | return res.status(400).end(); 83 | } 84 | } -------------------------------------------------------------------------------- /pages/api/notifications/[userId].ts: -------------------------------------------------------------------------------- 1 | import { NextApiRequest, NextApiResponse } from "next"; 2 | 3 | import prisma from '@/libs/prismadb'; 4 | 5 | export default async function handler(req: NextApiRequest, res: NextApiResponse) { 6 | if (req.method !== 'GET') { 7 | return res.status(405).end(); 8 | } 9 | 10 | try { 11 | const { userId } = req.query; 12 | 13 | if (!userId || typeof userId !== 'string') { 14 | throw new Error('Invalid ID'); 15 | } 16 | 17 | const notifications = await prisma.notification.findMany({ 18 | where: { 19 | userId, 20 | }, 21 | orderBy: { 22 | createdAt: 'desc' 23 | } 24 | }); 25 | 26 | await prisma.user.update({ 27 | where: { 28 | id: userId 29 | }, 30 | data: { 31 | hasNotification: false, 32 | } 33 | }); 34 | 35 | return res.status(200).json(notifications); 36 | } catch (error) { 37 | console.log(error); 38 | return res.status(400).end(); 39 | } 40 | } -------------------------------------------------------------------------------- /pages/api/posts/[postId].ts: -------------------------------------------------------------------------------- 1 | import { NextApiRequest, NextApiResponse } from "next"; 2 | 3 | import prisma from "@/libs/prismadb"; 4 | 5 | export default async function handler(req: NextApiRequest, res: NextApiResponse) { 6 | if (req.method !== 'GET') { 7 | return res.status(405).end(); 8 | } 9 | 10 | try { 11 | const { postId } = req.query; 12 | 13 | if (!postId || typeof postId !== 'string') { 14 | throw new Error('Invalid ID'); 15 | } 16 | 17 | const post = await prisma.post.findUnique({ 18 | where: { 19 | id: postId, 20 | }, 21 | include: { 22 | user: true, 23 | comments: { 24 | include: { 25 | user: true 26 | }, 27 | orderBy: { 28 | createdAt: 'desc' 29 | } 30 | }, 31 | }, 32 | }); 33 | 34 | return res.status(200).json(post); 35 | } catch (error) { 36 | console.log(error); 37 | return res.status(400).end(); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /pages/api/posts/index.ts: -------------------------------------------------------------------------------- 1 | import { NextApiRequest, NextApiResponse } from "next"; 2 | 3 | import serverAuth from "@/libs/serverAuth"; 4 | import prisma from "@/libs/prismadb"; 5 | 6 | export default async function handler(req: NextApiRequest, res: NextApiResponse) { 7 | if (req.method !== 'POST' && req.method !== 'GET') { 8 | return res.status(405).end(); 9 | } 10 | 11 | try { 12 | 13 | if (req.method === 'POST') { 14 | const { currentUser } = await serverAuth(req); 15 | const { body } = req.body; 16 | 17 | const post = await prisma.post.create({ 18 | data: { 19 | body, 20 | userId: currentUser.id 21 | } 22 | }); 23 | 24 | return res.status(200).json(post); 25 | } 26 | 27 | if (req.method === 'GET') { 28 | const { userId } = req.query; 29 | 30 | console.log({ userId }) 31 | 32 | let posts; 33 | 34 | if (userId && typeof userId === 'string') { 35 | posts = await prisma.post.findMany({ 36 | where: { 37 | userId 38 | }, 39 | include: { 40 | user: true, 41 | comments: true 42 | }, 43 | orderBy: { 44 | createdAt: 'desc' 45 | }, 46 | }); 47 | } else { 48 | posts = await prisma.post.findMany({ 49 | include: { 50 | user: true, 51 | comments: true 52 | }, 53 | orderBy: { 54 | createdAt: 'desc' 55 | } 56 | }); 57 | } 58 | 59 | return res.status(200).json(posts); 60 | } 61 | } catch (error) { 62 | console.log(error); 63 | return res.status(400).end(); 64 | } 65 | } -------------------------------------------------------------------------------- /pages/api/register.ts: -------------------------------------------------------------------------------- 1 | import bcrypt from 'bcrypt'; 2 | import { NextApiRequest, NextApiResponse } from "next"; 3 | 4 | import prisma from '@/libs/prismadb'; 5 | 6 | export default async function handler(req: NextApiRequest, res: NextApiResponse) { 7 | if (req.method !== 'POST') { 8 | return res.status(405).end(); 9 | } 10 | 11 | try { 12 | const { email, username, name, password } = req.body; 13 | 14 | const hashedPassword = await bcrypt.hash(password, 12); 15 | 16 | const user = await prisma.user.create({ 17 | data: { 18 | email, 19 | username, 20 | name, 21 | hashedPassword, 22 | } 23 | }); 24 | 25 | return res.status(200).json(user); 26 | } catch (error) { 27 | console.log(error); 28 | return res.status(400).end(); 29 | } 30 | } -------------------------------------------------------------------------------- /pages/api/users/[userId].ts: -------------------------------------------------------------------------------- 1 | import { NextApiRequest, NextApiResponse } from "next"; 2 | 3 | import prisma from '@/libs/prismadb'; 4 | 5 | export default async function handler(req: NextApiRequest, res: NextApiResponse) { 6 | if (req.method !== 'GET') { 7 | return res.status(405).end(); 8 | } 9 | 10 | try { 11 | const { userId } = req.query; 12 | 13 | if (!userId || typeof userId !== 'string') { 14 | throw new Error('Invalid ID'); 15 | } 16 | 17 | const existingUser = await prisma.user.findUnique({ 18 | where: { 19 | id: userId 20 | } 21 | }); 22 | 23 | const followersCount = await prisma.user.count({ 24 | where: { 25 | followingIds: { 26 | has: userId 27 | } 28 | } 29 | }) 30 | 31 | return res.status(200).json({ ...existingUser, followersCount }); 32 | } catch (error) { 33 | console.log(error); 34 | return res.status(400).end(); 35 | } 36 | }; 37 | -------------------------------------------------------------------------------- /pages/api/users/index.ts: -------------------------------------------------------------------------------- 1 | import { NextApiRequest, NextApiResponse } from "next"; 2 | 3 | import prisma from '@/libs/prismadb'; 4 | 5 | export default async function handler(req: NextApiRequest, res: NextApiResponse) { 6 | if (req.method !== 'GET') { 7 | return res.status(405).end(); 8 | } 9 | 10 | try { 11 | const users = await prisma.user.findMany({ 12 | orderBy: { 13 | createdAt: 'desc' 14 | } 15 | }); 16 | 17 | return res.status(200).json(users); 18 | } catch(error) { 19 | console.log(error); 20 | return res.status(400).end(); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /pages/index.tsx: -------------------------------------------------------------------------------- 1 | import PostFeed from "@/components/posts/PostFeed" 2 | import Header from "@/components/Header" 3 | import Form from "@/components/Form" 4 | 5 | export default function Home() { 6 | return ( 7 | <> 8 |
9 |
10 | 11 | 12 | ) 13 | } 14 | -------------------------------------------------------------------------------- /pages/notifications.tsx: -------------------------------------------------------------------------------- 1 | import Header from "@/components/Header"; 2 | import NotificationsFeed from "@/components/NotificationsFeed"; 3 | import useCurrentUser from "@/hooks/useCurrentUser"; 4 | import { NextPageContext } from "next"; 5 | import { getSession } from "next-auth/react"; 6 | 7 | export async function getServerSideProps(context: NextPageContext) { 8 | const session = await getSession(context); 9 | 10 | if (!session) { 11 | return { 12 | redirect: { 13 | destination: '/', 14 | permanent: false, 15 | } 16 | } 17 | } 18 | 19 | return { 20 | props: { 21 | session 22 | } 23 | } 24 | } 25 | 26 | const Notifications = () => { 27 | return ( 28 | <> 29 |
30 | 31 | 32 | ); 33 | } 34 | 35 | export default Notifications; -------------------------------------------------------------------------------- /pages/posts/[postId].tsx: -------------------------------------------------------------------------------- 1 | import { useRouter } from "next/router"; 2 | import { ClipLoader } from "react-spinners"; 3 | 4 | import usePost from "@/hooks/usePost"; 5 | 6 | import Header from "@/components/Header"; 7 | import Form from "@/components/Form"; 8 | import PostItem from "@/components/posts/PostItem"; 9 | import CommentFeed from "@/components/posts/CommentFeed"; 10 | 11 | 12 | const PostView = () => { 13 | const router = useRouter(); 14 | const { postId } = router.query; 15 | 16 | const { data: fetchedPost, isLoading } = usePost(postId as string); 17 | 18 | if (isLoading || !fetchedPost) { 19 | return ( 20 |
21 | 22 |
23 | ) 24 | } 25 | 26 | return ( 27 | <> 28 |
29 | 30 | 31 | 32 | 33 | ); 34 | } 35 | 36 | export default PostView; -------------------------------------------------------------------------------- /pages/search.tsx: -------------------------------------------------------------------------------- 1 | import Header from "@/components/Header"; 2 | 3 | const Search = () => { 4 | return ( 5 | <> 6 |
7 | 8 | ); 9 | } 10 | 11 | export default Search; -------------------------------------------------------------------------------- /pages/users/[userId].tsx: -------------------------------------------------------------------------------- 1 | import { useRouter } from "next/router"; 2 | import { ClipLoader } from "react-spinners"; 3 | 4 | import useUser from "@/hooks/useUser"; 5 | 6 | import PostFeed from "@/components/posts/PostFeed"; 7 | import Header from "@/components/Header"; 8 | import UserBio from "@/components/users/UserBio"; 9 | import UserHero from "@/components/users/UserHero"; 10 | 11 | 12 | 13 | const UserView = () => { 14 | const router = useRouter(); 15 | const { userId } = router.query; 16 | 17 | const { data: fetchedUser, isLoading } = useUser(userId as string); 18 | 19 | if (isLoading || !fetchedUser) { 20 | return ( 21 |
22 | 23 |
24 | ) 25 | } 26 | 27 | return ( 28 | <> 29 |
30 | 31 | 32 | 33 | 34 | ); 35 | } 36 | 37 | export default UserView; -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /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 | generator client { 5 | provider = "prisma-client-js" 6 | } 7 | 8 | datasource db { 9 | provider = "mongodb" 10 | url = env("DATABASE_URL") 11 | } 12 | 13 | model User { 14 | id String @id @default(auto()) @map("_id") @db.ObjectId 15 | name String? 16 | username String? @unique 17 | bio String? 18 | email String? @unique 19 | emailVerified DateTime? 20 | image String? 21 | coverImage String? 22 | profileImage String? 23 | hashedPassword String? 24 | createdAt DateTime @default(now()) 25 | updatedAt DateTime @updatedAt 26 | followingIds String[] @db.ObjectId 27 | hasNotification Boolean? 28 | 29 | posts Post[] 30 | comments Comment[] 31 | notifications Notification[] 32 | } 33 | 34 | model Post { 35 | id String @id @default(auto()) @map("_id") @db.ObjectId 36 | body String 37 | createdAt DateTime @default(now()) 38 | updatedAt DateTime @updatedAt 39 | userId String @db.ObjectId 40 | likedIds String[] @db.ObjectId 41 | image String? 42 | 43 | user User @relation(fields: [userId], references: [id], onDelete: Cascade) 44 | 45 | comments Comment[] 46 | } 47 | 48 | model Comment { 49 | id String @id @default(auto()) @map("_id") @db.ObjectId 50 | body String 51 | createdAt DateTime @default(now()) 52 | updatedAt DateTime @updatedAt 53 | userId String @db.ObjectId 54 | postId String @db.ObjectId 55 | 56 | user User @relation(fields: [userId], references: [id], onDelete: Cascade) 57 | post Post @relation(fields: [postId], references: [id], onDelete: Cascade) 58 | } 59 | 60 | model Notification { 61 | id String @id @default(auto()) @map("_id") @db.ObjectId 62 | body String 63 | userId String @db.ObjectId 64 | createdAt DateTime @default(now()) 65 | 66 | user User @relation(fields: [userId], references: [id], onDelete: Cascade) 67 | } -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mandeepsingh10/chwitter/f14505a851f9bb8a961b9069d45470de8197ecbf/public/favicon.ico -------------------------------------------------------------------------------- /public/images/placeholder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mandeepsingh10/chwitter/f14505a851f9bb8a961b9069d45470de8197ecbf/public/images/placeholder.png -------------------------------------------------------------------------------- /public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/thirteen.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /styles/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | :root { 6 | @apply h-full bg-black 7 | } 8 | 9 | body { 10 | @apply h-full bg-black 11 | } 12 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | content: [ 4 | "./app/**/*.{js,ts,jsx,tsx}", 5 | "./pages/**/*.{js,ts,jsx,tsx}", 6 | "./components/**/*.{js,ts,jsx,tsx}", 7 | ], 8 | theme: { 9 | extend: {}, 10 | }, 11 | plugins: [], 12 | } 13 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "noEmit": true, 10 | "esModuleInterop": true, 11 | "module": "esnext", 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "jsx": "preserve", 16 | "incremental": true, 17 | "baseUrl": ".", 18 | "paths": { 19 | "@/*": ["./*"] 20 | } 21 | }, 22 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], 23 | "exclude": ["node_modules"] 24 | } 25 | --------------------------------------------------------------------------------