├── .env.example ├── .eslintrc.json ├── .github └── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── .gitignore ├── .idea ├── .gitignore ├── discord.xml ├── inspectionProfiles │ └── Project_Default.xml ├── modules.xml ├── twitter-clone.iml └── vcs.xml ├── LICENSE ├── README.md ├── base ├── button.ts ├── colors.ts └── spaces.ts ├── components ├── ActionSidebar.tsx ├── Avatar.tsx ├── ImageUpload.tsx ├── Layout.tsx ├── PostForm.tsx ├── Searchbar.tsx ├── Sidebar.tsx ├── SidebarItem.tsx ├── SidebarLogo.tsx ├── Splash.tsx ├── WhoToFollow.tsx ├── bottom │ ├── Bottom.tsx │ └── BottomTitle.tsx ├── follow │ ├── UserFollowers.tsx │ └── UserFollowing.tsx ├── modals │ ├── EditModal.tsx │ ├── LoginModal.tsx │ ├── RegisterModal.tsx │ └── TweetModal.tsx ├── notifications │ └── NotificationFeed.tsx ├── posts │ ├── CommentFeed.tsx │ ├── CommentItem.tsx │ ├── PostFeed.tsx │ └── PostFeeds.tsx ├── shared │ ├── BasicDatePicker.tsx │ ├── Button.tsx │ ├── FollowPage.tsx │ ├── Header.tsx │ ├── Input.tsx │ ├── Loading.tsx │ ├── Modal.tsx │ └── Portal.tsx └── users │ ├── UserHero.tsx │ └── UserInfo.tsx ├── cron.sh ├── hooks ├── useBottomBar.ts ├── useCurrentUser.ts ├── useEditModal.ts ├── useFollow.ts ├── useFollowingDetails.ts ├── useLikes.ts ├── useLoginModal.ts ├── useNotifications.ts ├── usePost.ts ├── usePosts.ts ├── useRegisterModal.ts ├── useSearch.ts ├── useTweetActionModal.ts ├── useUser.ts ├── useUsers.ts └── useWindowSize.ts ├── libs ├── fetcher.ts ├── prismadb.ts └── serverAuth.ts ├── next.config.js ├── package-lock.json ├── package.json ├── pages ├── 404.tsx ├── _app.tsx ├── api │ ├── auth │ │ └── [...nextauth].ts │ ├── comments.ts │ ├── current.ts │ ├── edit.ts │ ├── follow.ts │ ├── following.ts │ ├── likes.ts │ ├── notifications │ │ └── [userId].ts │ ├── posts │ │ ├── [postId].ts │ │ └── index.ts │ ├── register.ts │ ├── search.ts │ └── users │ │ ├── [username].ts │ │ └── index.ts ├── connect.tsx ├── index.tsx ├── notifications.tsx ├── posts │ └── [postId].tsx └── users │ └── [username] │ ├── followers.tsx │ ├── following.tsx │ └── index.tsx ├── postcss.config.js ├── prisma └── schema.prisma ├── public ├── change-image.svg ├── close.svg ├── default_doge_coin.png ├── favicon.ico ├── new-twitter-favicon.ico ├── twitter-favicon.ico └── twitter-user-avatar.jpg ├── styles └── globals.css ├── tailwind.config.js ├── tsconfig.json ├── types ├── sidebar.type.ts └── user.type.ts ├── utils ├── @fake.db.ts ├── constants.ts ├── enums.ts └── helpers.ts └── yarn.lock /.env.example: -------------------------------------------------------------------------------- 1 | # Environment variables declared in this file are automatically made available to Prisma. 2 | # See the documentation for more detail: https://pris.ly/d/prisma-schema#accessing-environment-variables-from-the-schema 3 | 4 | # Prisma supports the native connection string format for PostgreSQL, MySQL, SQLite, SQL Server, MongoDB and CockroachDB. 5 | # See the documentation for all the connection string options: https://pris.ly/d/connection-strings 6 | 7 | DATABASE_URL= 8 | NEXTAUTH_JWT_SECRET= 9 | NEXTAUTH_SECRET= -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.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 | .pnpm-debug.log* 27 | 28 | # local env files 29 | .env*.local 30 | 31 | .env 32 | 33 | # vercel 34 | .vercel 35 | 36 | # typescript 37 | *.tsbuildinfo 38 | next-env.d.ts 39 | -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | # Editor-based HTTP Client requests 5 | /httpRequests/ 6 | -------------------------------------------------------------------------------- /.idea/discord.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/Project_Default.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/twitter-clone.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Harun Doğdu 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 | logo 4 | 5 | # Twitter Clone 6 | This is a [Twitter](https://twitter.com) Clone project built with [Next.js](https://nextjs.org/), [Prisma](https://www.prisma.io/), [MongoDb](https://www.mongodb.com/), [Tailwind](https://tailwindcss.com/), [Typescript](https://www.typescriptlang.org/) and [NextAuth](https://next-auth.js.org/) libraries. It is a full-stack project that uses [Next.js](https://nextjs.org/) for the frontend and [Prisma](https://www.prisma.io/) for the backend. It is a Twitter clone that allows users to create an account, login, logout, follow other users, like and retweet tweets, and more. 7 | 8 | 9 | 10 | 11 | ![](https://img.shields.io/website-up-down-green-red/http/monip.org.svg) 12 | ![](https://img.shields.io/badge/Maintained-Yes-indigo) 13 | ![](https://img.shields.io/github/forks/harundogdu/twitter-clone.svg) 14 | ![](https://img.shields.io/github/stars/harundogdu/twitter-clone.svg) 15 | ![](https://img.shields.io/github/issues/harundogdu/twitter-clone) 16 | ![](https://img.shields.io/github/last-commit/harundogdu/twitter-clone) 17 | 18 |

19 | View Demo 20 | · 21 | Documentation 22 | · 23 | Report Bug 24 | · 25 | Request Feature 26 |

27 | 28 |
29 | 30 |
31 | 32 | 33 | 34 | ## :star2: About the Project 35 | 36 | 37 | 38 |
39 | image 40 |
41 | 42 |
43 | 44 | ## :toolbox: Getting Started 45 | 46 | ### :bangbang: Prerequisites 47 | 48 | - Sign up for a MongoDB account HERE 49 | - Install Node JS in your computer HERE 50 | 51 | 52 | 53 | ### :key: Environment Variables 54 | 55 | To run this project, you will need to add the following environment variables to your .env file 56 | 57 | `DATABASE_URL` 58 | 59 | `NEXTAUTH_JWT_SECRET` 60 | 61 | `NEXTAUTH_SECRET` 62 | 63 | These instructions will get you a copy of the project up and running on your local machine for development and testing purposes. 64 | 65 | ## :gear: Installation 66 | 67 | A step by step series of examples that tell you how to get a development env running 68 | Clone the repository 69 | 70 | ```bash 71 | git clone https://github.com/yourusername/twitter-clone.git 72 | ``` 73 | 74 |

Install dependencies

75 | 76 | ```bash 77 | npm install 78 | ``` 79 | 80 |

Generate Prisma Client with the following command

81 | 82 | ```bash 83 | npx prisma generate 84 | ``` 85 | 86 |

Run the development server

87 | 88 | ```bash 89 | npm run dev 90 | ``` 91 | 92 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 93 | 94 | ## Available Scripts 95 | 96 | 102 | 103 | ## :notebook_with_decorative_cover: Prisma 104 | 105 | Prisma is an open-source database toolkit that makes it easy for developers to reason about their data and how they access it. It is used to query a database inside a Node.js or TypeScript application. 106 | 107 | ## :notebook_with_decorative_cover: Prisma Schema 108 | 109 | The Prisma schema is the single source of truth for your database schema. It describes your database tables, columns, and relations. It also defines which operations are available on your data. 110 | 111 | ## :notebook_with_decorative_cover: Prisma Client 112 | 113 | Prisma Client is an auto-generated and type-safe query builder for Node.js & TypeScript. It's used as an alternative to writing plain SQL, or using another database access tool such as SQL query builders (e.g. SQLAlchemy) or ORMs (e.g. TypeORM). 114 | 115 | ## :space_invader: Built With 116 | 117 | 148 | 149 | ## :handshake: Authors 150 | 151 | 157 | 158 | ## Deploy on Vercel 159 | 160 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. 161 | 162 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. 163 | -------------------------------------------------------------------------------- /base/button.ts: -------------------------------------------------------------------------------- 1 | const ButtonUtils = { 2 | styles: { 3 | primary: "bg-primary-main text-custom-white", 4 | secondary: "bg-custom-white text-custom-black", 5 | blackBtn: 6 | "bg-custom-black text-custom-white !border-sm border-custom-white ", 7 | }, 8 | buttonSizes: { 9 | customButtonStyle: "px-4 py-2 text-xs", 10 | smStyle: "px-5 py-1.5 text-sm", 11 | mdStyle: "px-8 py-2 text-base", 12 | lgStyle: "px-8 py-3 text-lg", 13 | }, 14 | sizes: { 15 | xs: "w-16", 16 | sm: "w-24", 17 | md: "w-32", 18 | lg: "w-40", 19 | }, 20 | }; 21 | export default ButtonUtils; 22 | -------------------------------------------------------------------------------- /base/colors.ts: -------------------------------------------------------------------------------- 1 | const ColorUtils = { 2 | colors: { 3 | blue: "#1f9bf0", 4 | green: "#05b97c", 5 | pink: "#f9197f", 6 | yellow: "#ffd401", 7 | purple: "#7956ff", 8 | orange: "#ff7a00", 9 | red: "#ff0000", 10 | white: "#ffffff", 11 | black: "#000000", 12 | gray: "#f2f2f2", 13 | darkGray: "#333333", 14 | lightGray: "#999999", 15 | main: "#1f9bf0", 16 | }, 17 | darken: (color: string, percent: number) => { 18 | const num = parseInt(color.replace("#", ""), 16), 19 | amt = Math.round(2.55 * percent), 20 | R = (num >> 16) + amt, 21 | G = ((num >> 8) & 0x00ff) + amt, 22 | B = (num & 0x0000ff) + amt; 23 | return ( 24 | "#" + 25 | ( 26 | 0x1000000 + 27 | (R < 255 ? (R < 1 ? 0 : R) : 255) * 0x10000 + 28 | (G < 255 ? (G < 1 ? 0 : G) : 255) * 0x100 + 29 | (B < 255 ? (B < 1 ? 0 : B) : 255) 30 | ) 31 | .toString(16) 32 | .slice(1) 33 | ); 34 | }, 35 | lighten: (color: string, percent: number) => { 36 | const num = parseInt(color.replace("#", ""), 16), 37 | amt = Math.round(2.55 * percent), 38 | R = (num >> 16) - amt, 39 | G = ((num >> 8) & 0x00ff) - amt, 40 | B = (num & 0x0000ff) - amt; 41 | return ( 42 | "#" + 43 | ( 44 | 0x1000000 + 45 | (R > 255 ? (R > 1 ? 0 : R) : 255) * 0x10000 + 46 | (G > 255 ? (G > 1 ? 0 : G) : 255) * 0x100 + 47 | (B > 255 ? (B > 1 ? 0 : B) : 255) 48 | ) 49 | .toString(16) 50 | .slice(1) 51 | ); 52 | }, 53 | }; 54 | 55 | export default ColorUtils; 56 | -------------------------------------------------------------------------------- /base/spaces.ts: -------------------------------------------------------------------------------- 1 | const SpaceUtils = { 2 | spaces: { 3 | xss: 2, 4 | xs: 4, 5 | sm: 8, 6 | md: 16, 7 | lg: 24, 8 | xl: 32, 9 | xxl: 40, 10 | } 11 | }; 12 | 13 | export default SpaceUtils; -------------------------------------------------------------------------------- /components/ActionSidebar.tsx: -------------------------------------------------------------------------------- 1 | import { useRouter } from "next/router"; 2 | import { useEffect, useState, useRef } from "react"; 3 | 4 | import { IUser } from "@/types/user.type"; 5 | 6 | import SearchBar from "./Searchbar"; 7 | import WhoToFollow from "./WhoToFollow"; 8 | import useCurrentUser from "@/hooks/useCurrentUser"; 9 | import useUsers from "@/hooks/useUsers"; 10 | 11 | const ActionSidebar = () => { 12 | const { data: allUsers = [] } = useUsers(); 13 | const { data: currentUser } = useCurrentUser(); 14 | const router = useRouter(); 15 | const [suggestedUsers, setSuggestedUsers] = useState([]); 16 | const [isOpen, setIsOpen] = useState(false); 17 | const moreRef = useRef(null); 18 | 19 | useEffect(() => { 20 | if (allUsers.length > 0 && currentUser) { 21 | const shuffledUsers = [...allUsers]; 22 | 23 | shuffledUsers.sort(() => Math.random() * allUsers.length); 24 | 25 | const selectedUsers = shuffledUsers.slice(0, 3); 26 | setSuggestedUsers(selectedUsers); 27 | } 28 | 29 | 30 | }, [allUsers, currentUser, moreRef]); 31 | 32 | if (suggestedUsers.length <= 0 || !currentUser) { 33 | return null; 34 | } 35 | 36 | if (router.pathname === "/connect") { 37 | return null; 38 | } 39 | 40 | 41 | 42 | return ( 43 |
44 | 45 | 46 |
47 | ); 48 | }; 49 | 50 | export default ActionSidebar; 51 | -------------------------------------------------------------------------------- /components/Avatar.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC, useCallback } from "react"; 2 | 3 | import Image from "next/image"; 4 | import { useRouter } from "next/router"; 5 | 6 | import useUser from "@/hooks/useUser"; 7 | 8 | import { AvatarSize } from "@/utils/enums"; 9 | 10 | interface AvatarProps { 11 | username: string; 12 | size?: "small" | "medium" | "large"; 13 | hasBorder?: boolean; 14 | clickable?: boolean; 15 | } 16 | 17 | const Avatar: FC = ({ 18 | username, 19 | size = "medium", 20 | hasBorder = false, 21 | clickable = true, 22 | }) => { 23 | const router = useRouter(); 24 | const { data: fetchedUser } = useUser(username); 25 | 26 | const handleClick = useCallback( 27 | async (event: React.MouseEvent) => { 28 | event.stopPropagation(); 29 | const response = await fetch(`/api/users/${username}`); 30 | const data = await response.json(); 31 | 32 | const _username: string = data.username; 33 | 34 | if (clickable) { 35 | const url = `/users/${_username}`; 36 | router.push(url); 37 | } 38 | }, 39 | [router, username, clickable] 40 | ); 41 | 42 | return ( 43 |
44 | {`${fetchedUser?.name} 75 |
76 | ); 77 | }; 78 | 79 | export default Avatar; 80 | -------------------------------------------------------------------------------- /components/ImageUpload.tsx: -------------------------------------------------------------------------------- 1 | import Image from "next/image"; 2 | import React, { FC, useCallback, useState } from "react"; 3 | 4 | import { useDropzone } from "react-dropzone"; 5 | 6 | interface ImageUploadProps { 7 | label: string; 8 | value: string; 9 | onChange: (base64: string) => void; 10 | disabled?: boolean; 11 | } 12 | 13 | const ImageUpload: FC = ({ 14 | value, 15 | onChange, 16 | label, 17 | disabled, 18 | }) => { 19 | const [base64Image, setBase64Image] = useState(value); 20 | 21 | const handleDrop = useCallback( 22 | (acceptedFiles: File[]) => { 23 | const file = acceptedFiles[0]; 24 | 25 | const reader = new FileReader(); 26 | 27 | reader.readAsDataURL(file); 28 | 29 | reader.onload = () => { 30 | setBase64Image(reader.result as string); 31 | onChange(reader.result as string); 32 | }; 33 | 34 | reader.onerror = () => { 35 | console.error("Something went wrong!"); 36 | }; 37 | }, 38 | [onChange] 39 | ); 40 | 41 | const { getRootProps, getInputProps } = useDropzone({ 42 | accept: { 43 | "image/png": [], 44 | "image/jpeg": [], 45 | }, 46 | onDrop: handleDrop, 47 | disabled, 48 | maxFiles: 1, 49 | }); 50 | 51 | return ( 52 |
56 | 57 | {base64Image ? ( 58 |
59 | {label === "Profile Image" ? ( 60 | <> 61 |
62 |
63 | change-image 68 |
69 | Uploaded image 76 | 77 | ) : ( 78 | <> 79 |
80 |
81 | change-image 86 |
87 |
88 | change-image 93 |
94 | Uploaded image 101 | 102 | )} 103 |
104 | ) : ( 105 |

{label}

106 | )} 107 |
108 | ); 109 | }; 110 | 111 | export default ImageUpload; 112 | -------------------------------------------------------------------------------- /components/Layout.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from "react"; 2 | 3 | import ActionSidebar from "@/components/ActionSidebar"; 4 | import Sidebar from "@/components/Sidebar"; 5 | interface ILayoutProps { 6 | children: React.ReactNode; 7 | } 8 | 9 | const Layout: FC = ({ children }) => { 10 | return ( 11 |
15 |
16 |
17 | 18 |
19 | {children} 20 |
21 |
22 | 23 |
24 |
25 |
26 |
27 | ); 28 | }; 29 | 30 | export default Layout; 31 | -------------------------------------------------------------------------------- /components/PostForm.tsx: -------------------------------------------------------------------------------- 1 | import { FC, useCallback, useState, useEffect } from "react"; 2 | 3 | import axios from "axios"; 4 | import { toast } from "react-hot-toast"; 5 | import { CircularProgressbar, buildStyles } from "react-circular-progressbar"; 6 | import "react-circular-progressbar/dist/styles.css"; 7 | 8 | import Avatar from "./Avatar"; 9 | import Button from "./shared/Button"; 10 | 11 | import useCurrentUser from "@/hooks/useCurrentUser"; 12 | import useLoginModal from "@/hooks/useLoginModal"; 13 | import usePosts from "@/hooks/usePosts"; 14 | import useRegisterModal from "@/hooks/useRegisterModal"; 15 | import usePost from "@/hooks/usePost"; 16 | 17 | interface IPostFormProps { 18 | placeholder: string; 19 | isComment?: boolean; 20 | username?: string; 21 | postId?: string; 22 | } 23 | 24 | const PostForm: FC = ({ 25 | placeholder, 26 | isComment, 27 | username, 28 | postId, 29 | }) => { 30 | const loginModal = useLoginModal(); 31 | const registerModal = useRegisterModal(); 32 | const { data: currentUser } = useCurrentUser(); 33 | const { mutate: mutatePosts } = usePosts(username as string); 34 | const { mutate: mutatePost } = usePost(postId as string); 35 | const { data: isLoggedIn } = useCurrentUser(); 36 | 37 | const [percentage, setPercentage] = useState(0); 38 | const [body, setBody] = useState(""); 39 | const [loading, setLoading] = useState(false); 40 | 41 | const handleLoginClick = useCallback(() => { 42 | loginModal.onOpen(); 43 | }, [loginModal]); 44 | 45 | const handleRegisterClick = useCallback(() => { 46 | registerModal.onOpen(); 47 | }, [registerModal]); 48 | 49 | const handleSubmit = useCallback(async () => { 50 | try { 51 | setLoading(true); 52 | 53 | const url = isComment ? `/api/comments?postId=${postId}` : `/api/posts`; 54 | 55 | await axios.post(url, { body }); 56 | 57 | toast.success("Post created!"); 58 | 59 | setBody(""); 60 | mutatePosts(); 61 | mutatePost(); 62 | } catch (error: any) { 63 | console.log(error); 64 | toast.error("Something went wrong!"); 65 | } finally { 66 | setLoading(false); 67 | } 68 | }, [body, isComment, mutatePosts, mutatePost, postId]); 69 | 70 | const getProgressbarStyle = () => { 71 | if (body.length > 0 && body.length < 80) { 72 | return buildStyles({ 73 | rotation: 0, 74 | strokeLinecap: "butt", 75 | pathTransitionDuration: 0, 76 | trailColor: "#2F3336", 77 | pathColor: "#1D9BF0", 78 | }); 79 | } 80 | if (body.length >= 80 && body.length < 100) { 81 | return buildStyles({ 82 | rotation: 0, 83 | strokeLinecap: "butt", 84 | pathTransitionDuration: 0, 85 | textSize: "40px", 86 | textColor: "#71767b", 87 | trailColor: "#2F3336", 88 | pathColor: "#FFD400", 89 | }); 90 | } 91 | if (body.length >= 100) { 92 | return buildStyles({ 93 | rotation: 0, 94 | strokeLinecap: "butt", 95 | pathTransitionDuration: 0, 96 | textSize: "40px", 97 | textColor: "#F4212E", 98 | trailColor: "#2F3336", 99 | pathColor: "#F4212E", 100 | }); 101 | } 102 | }; 103 | 104 | useEffect(() => { 105 | const calculatePercentage = () => { 106 | const currentLength = body.length; 107 | const maxLength = 100; 108 | const calculatedPercentage = (currentLength / maxLength) * 100; 109 | 110 | setPercentage(calculatedPercentage); 111 | }; 112 | 113 | calculatePercentage(); 114 | }, [body]); 115 | 116 | return ( 117 | <> 118 | {!isLoggedIn ? ( 119 |
120 |

121 | Welcome to Twitter 122 |

123 |
124 |
125 |
133 |
134 |
143 |
144 |
145 | ) : ( 146 |
147 |
148 | 149 |
150 |
151 | 158 |
164 |
170 |
171 | {body.length > 0 && body.length < 80 && body.trim() ? ( 172 | 177 | ) : body.length >= 80 && body.trim() ? ( 178 | 184 | ) : null} 185 |
186 |
195 |
196 |
197 | )} 198 | 199 | ); 200 | }; 201 | 202 | export default PostForm; 203 | -------------------------------------------------------------------------------- /components/Searchbar.tsx: -------------------------------------------------------------------------------- 1 | import { useCallback, useEffect, useState, useRef } from "react"; 2 | 3 | import { debounce } from "lodash"; 4 | import { useRouter } from "next/router"; 5 | import { RiSearchLine, RiCloseFill } from "react-icons/ri"; 6 | 7 | import Avatar from "@/components/Avatar"; 8 | 9 | import { IUser } from "@/types/user.type"; 10 | 11 | import useSearch from "@/hooks/useSearch"; 12 | 13 | const SearchBar = () => { 14 | const [searchResults, setSearchResults] = useState([]); 15 | const [searchValue, setSearchValue] = useState(""); 16 | const [searchbarOn, setSearchbarOn] = useState(false); 17 | 18 | 19 | 20 | const isBackspaceDown = useRef(false); 21 | 22 | 23 | const router = useRouter(); 24 | const { searchUsers } = useSearch(); 25 | 26 | // eslint-disable-next-line react-hooks/exhaustive-deps 27 | const searchOnChange = useCallback( 28 | debounce(async (searchValue) => { 29 | const searchText = searchValue!.target.value; 30 | if (!isBackspaceDown.current) { 31 | await getUsers(searchText); 32 | } 33 | }, 400), 34 | [searchUsers] 35 | ); 36 | 37 | const getUsers = async (searchText: string) => { 38 | if (searchText.length > 0) { 39 | const users = await searchUsers(searchText); 40 | setSearchResults(users); 41 | } else { 42 | setSearchResults([]); 43 | } 44 | }; 45 | 46 | const handleCloseSearchbar = (e: React.MouseEvent) => { 47 | const target = e.target as HTMLElement; 48 | const attributeValue = target.getAttribute("image-data"); 49 | if (attributeValue === "searchbar") return; 50 | setSearchbarOn(false); 51 | 52 | }; 53 | 54 | const handleKeyDown = (event: React.KeyboardEvent) => { 55 | if (event.key === "Backspace") { 56 | isBackspaceDown.current = true; 57 | } 58 | }; 59 | 60 | const handleKeyUp = (event: React.KeyboardEvent) => { 61 | if (event.key === "Backspace") { 62 | isBackspaceDown.current = false; 63 | } 64 | }; 65 | 66 | useEffect(() => { 67 | searchOnChange({ target: { value: searchValue } }); 68 | }, [searchValue]); 69 | 70 | 71 | 72 | return ( 73 | 74 | 75 |
76 | { 77 | searchbarOn && ( 78 |
{handleCloseSearchbar(e)}}/> 79 | ) 80 | } 81 |
82 | 83 | { 88 | setSearchValue(e.target.value); 89 | }} 90 | onClick={() => setSearchbarOn(true)} 91 | value={searchValue} 92 | searchbar-data="searchbar" 93 | onKeyDown={handleKeyDown} 94 | onKeyUp={handleKeyUp} 95 | /> 96 | {searchResults.length === 0 && searchbarOn && ( 97 |
101 |
102 |

103 | Try searching for people, topics, or keywords 104 |

105 |
106 |
107 | )} 108 | 109 | {searchResults.length > 0 && searchbarOn && ( 110 | { 113 | e.preventDefault(); 114 | setSearchValue(""); 115 | setSearchResults([]); 116 | }} 117 | 118 | /> 119 | )} 120 |
121 | {searchResults.length > 0 && searchbarOn && ( 122 |
6 ?"shadow-customSecondary rounded-lg max-h-96 overflow-y-scroll overflow-x-hidden scrollbar-thin scrollbar-thumb-neutral-500 scrollbar-track-neutral-800 scrollbar-thumb-rounded-md scrollbar-track-rounded-sm":"shadow-customSecondary rounded-lg max-h-96 " } 124 | 125 | > 126 | {searchResults.map((user: IUser) => { 127 | return ( 128 |
{ 130 | router.push(`/users/${user.username}`); 131 | searchUsers(""); 132 | }} 133 | key={user.id} 134 | className="flex items-center gap-4 justify-between py-2 px-4 hover:bg-neutral-700 hover:bg-opacity-70 cursor-pointer duration-200" 135 | > 136 | 137 |
138 |

142 | {user.name} 143 |

144 |
145 | @{user.username} 146 |
147 |
148 |
149 | ); 150 | })} 151 |
152 | )} 153 |
154 |
155 |
156 | ); 157 | }; 158 | 159 | export default SearchBar; 160 | -------------------------------------------------------------------------------- /components/Sidebar.tsx: -------------------------------------------------------------------------------- 1 | import { useCallback } from "react"; 2 | import { useRouter } from "next/router"; 3 | 4 | import { RiMoreFill } from "react-icons/ri"; 5 | 6 | import useLoginModal from "@/hooks/useLoginModal"; 7 | import useCurrentUser from "@/hooks/useCurrentUser"; 8 | import useTweetActionModal from "@/hooks/useTweetActionModal"; 9 | 10 | import SidebarLogo from "@/components/SidebarLogo"; 11 | import SidebarItem from "@/components/SidebarItem"; 12 | import Button from "@/components/shared/Button"; 13 | 14 | import { SidebarItems } from "@/utils/@fake.db"; 15 | 16 | import Avatar from "./Avatar"; 17 | 18 | const Sidebar = () => { 19 | const { onOpen } = useLoginModal(); 20 | const { onOpen: tweetModal } = useTweetActionModal(); 21 | const { data: currentUser } = useCurrentUser(); 22 | const router = useRouter(); 23 | 24 | const handleShareClick = useCallback(() => { 25 | if (!currentUser?.email) { 26 | return onOpen(); 27 | } else { 28 | return tweetModal(); 29 | } 30 | }, [currentUser?.email, onOpen, tweetModal]); 31 | 32 | const RenderSidebarItems = useCallback(() => { 33 | const sideBarItems = currentUser?.email 34 | ? [...SidebarItems] 35 | .filter((item) => item.active) 36 | .filter((item) => { 37 | if (item.href === "/notifications") { 38 | item.alert = currentUser?.hasNotification; 39 | } 40 | return item; 41 | }) 42 | : [...SidebarItems] 43 | .filter((item) => item.active) 44 | .filter((item) => item.public); 45 | 46 | return ( 47 |
48 | {sideBarItems.map((item, index) => ( 49 | 58 | ))} 59 |
60 | ); 61 | }, [currentUser?.email, currentUser?.hasNotification]); 62 | return ( 63 |
64 |
65 |
66 |
67 | 68 | 69 |
78 | {currentUser && ( 79 |
router.push("/users/" + currentUser?.username)} 82 | > 83 |
84 | 85 |
86 | <> 87 |
88 |
89 | {currentUser?.name} 90 |
91 |
92 | @{currentUser?.username} 93 |
94 |
95 |
96 | 97 |
98 | 99 |
100 | )} 101 |
102 |
103 |
104 | ); 105 | }; 106 | 107 | export default Sidebar; 108 | -------------------------------------------------------------------------------- /components/SidebarItem.tsx: -------------------------------------------------------------------------------- 1 | import { FC, useCallback } from "react"; 2 | import { useRouter } from "next/router"; 3 | 4 | import { BsDot } from "react-icons/bs"; 5 | 6 | import { ISidebarType } from "@/types/sidebar.type"; 7 | 8 | import useWindowSize from "@/hooks/useWindowSize"; 9 | import useLoginModal from "@/hooks/useLoginModal"; 10 | import useCurrentUser from "@/hooks/useCurrentUser"; 11 | 12 | const SidebarItem: FC = ({ 13 | href, 14 | onClick, 15 | label, 16 | icon: Icon, 17 | secondaryIcon: SecondaryIcon, 18 | public: isPublic, 19 | alert, 20 | }) => { 21 | const { width } = useWindowSize(); 22 | const router = useRouter(); 23 | const loginModal = useLoginModal(); 24 | const { data: currentUser } = useCurrentUser(); 25 | 26 | const handleSidebarItemClick = useCallback(() => { 27 | if (onClick) { 28 | return onClick(); 29 | } 30 | 31 | if (isPublic && !currentUser?.email) { 32 | return loginModal.onOpen(); 33 | } else if (href) { 34 | let redirectUrl = href; 35 | if (label.localeCompare("Profile") == 0) { 36 | redirectUrl = href + currentUser?.username; 37 | } 38 | router.push(redirectUrl); 39 | } 40 | }, [ 41 | href, 42 | onClick, 43 | router, 44 | loginModal, 45 | isPublic, 46 | currentUser?.email, 47 | label, 48 | currentUser?.username, 49 | ]); 50 | 51 | const RenderIcon = useCallback(() => { 52 | return width! < 1024 ? ( 53 | SecondaryIcon ? ( 54 | 55 | ) : ( 56 | <> 57 | 58 | {alert ? ( 59 | 60 | ) : null} 61 | 62 | ) 63 | ) : ( 64 | <> 65 | 66 | {alert ? ( 67 | 68 | ) : null} 69 | 70 | ); 71 | }, [width, SecondaryIcon, Icon, alert]); 72 | 73 | return ( 74 |
78 |
79 | 80 |
81 |
82 | 83 | {alert ? ( 84 | 85 | ) : null} 86 | {label} 87 |
88 |
89 | ); 90 | }; 91 | 92 | export default SidebarItem; 93 | -------------------------------------------------------------------------------- /components/SidebarLogo.tsx: -------------------------------------------------------------------------------- 1 | import { FaTwitter } from "react-icons/fa"; 2 | import { useRouter } from "next/router"; 3 | 4 | const SidebarLogo = () => { 5 | const router = useRouter(); 6 | const handleOnClick = () => { 7 | router.push("/"); 8 | }; 9 | return ( 10 |
14 | 15 |
16 | ); 17 | }; 18 | 19 | export default SidebarLogo; 20 | -------------------------------------------------------------------------------- /components/Splash.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from "react"; 2 | 3 | import { FaTwitter } from "react-icons/fa"; 4 | 5 | import ColorUtils from "@/base/colors"; 6 | 7 | const Splash = () => { 8 | const [showSplash, setShowSplash] = useState(true); 9 | 10 | useEffect(() => { 11 | const timeout = setTimeout(() => { 12 | setShowSplash(false); 13 | }, 2000); 14 | 15 | return () => clearTimeout(timeout); 16 | }, []); 17 | 18 | return showSplash === true ? ( 19 |
23 |
27 |
28 | 29 |
30 |
31 |
32 | ) : null; 33 | }; 34 | 35 | export default Splash; 36 | -------------------------------------------------------------------------------- /components/WhoToFollow.tsx: -------------------------------------------------------------------------------- 1 | import { useRouter } from "next/router"; 2 | import { useState, useRef, useEffect } from "react"; 3 | 4 | import { IUser } from "@/types/user.type"; 5 | 6 | import ColorUtils from "@/base/colors"; 7 | 8 | import useCurrentUser from "@/hooks/useCurrentUser"; 9 | import useUsers from "@/hooks/useUsers"; 10 | 11 | import Avatar from "@/components/Avatar"; 12 | import Button from "@/components/shared/Button"; 13 | interface WhoToFollowProps { 14 | suggestedUsers: IUser[]; 15 | } 16 | 17 | const WhoToFollow = ({ suggestedUsers }: WhoToFollowProps) => { 18 | const { data: allUsers = [] } = useUsers(); 19 | const { data: currentUser } = useCurrentUser(); 20 | const router = useRouter(); 21 | const [isOpen, setIsOpen] = useState(false); 22 | const moreRef = useRef(null); 23 | 24 | const togglePopup = () => { 25 | setIsOpen(!isOpen); 26 | }; 27 | 28 | useEffect(() => { 29 | const handleClickOutside = (event: MouseEvent) => { 30 | if (moreRef.current && !moreRef.current.contains(event.target as Node)) { 31 | setIsOpen(false); 32 | } 33 | }; 34 | 35 | document.addEventListener("mousedown", handleClickOutside); 36 | 37 | return () => { 38 | document.removeEventListener("mousedown", handleClickOutside); 39 | }; 40 | }, [allUsers, currentUser, moreRef]); 41 | 42 | const currentDate = new Date(); 43 | const year = currentDate.getFullYear(); 44 | const month = String(currentDate.getMonth() + 1).padStart(2, "0"); 45 | const day = String(currentDate.getDate()).padStart(2, "0"); 46 | const formattedDate = `${year}-${month}-${day}`; 47 | 48 | return ( 49 |
50 |
51 |
52 |

53 | Who to follow 54 |

55 |
56 | {suggestedUsers.map((user: IUser) => { 57 | return ( 58 |
{ 60 | router.push(`/users/${user.username}`); 61 | }} 62 | key={user.id} 63 | className="flex items-center gap-4 justify-between py-2 px-4 hover:bg-neutral-700 hover:bg-opacity-70 cursor-pointer duration-200" 64 | > 65 | 66 |
67 |

71 | {user.name} 72 |

73 |
74 | @{user.username} 75 |
76 |
77 |
78 |
86 |
87 | ); 88 | })} 89 |
router.push("/connect")} 92 | > 93 |

94 | 99 | Show more 100 | 101 |

102 |
103 |
104 |
105 |
106 | 174 |
175 |
176 |
177 | ); 178 | }; 179 | 180 | export default WhoToFollow; 181 | -------------------------------------------------------------------------------- /components/bottom/Bottom.tsx: -------------------------------------------------------------------------------- 1 | import ColorUtils from "@/base/colors"; 2 | 3 | import useLoginModal from "@/hooks/useLoginModal"; 4 | import useRegisterModal from "@/hooks/useRegisterModal"; 5 | 6 | import Button from "@/components/shared/Button"; 7 | import BottomTitle from "./BottomTitle"; 8 | import { useCallback } from "react"; 9 | 10 | const Bottom = () => { 11 | const loginModal = useLoginModal(); 12 | const registerModal = useRegisterModal(); 13 | 14 | const handleLoginClick = useCallback(() => { 15 | loginModal.onOpen(); 16 | }, [loginModal]); 17 | 18 | const handleRegisterClick = useCallback(() => { 19 | registerModal.onOpen(); 20 | }, [registerModal]); 21 | 22 | return ( 23 |
24 |
30 |
31 |
32 |
33 | 34 | 39 |
40 |
41 |
42 |
52 |
53 |
62 |
63 |
64 |
65 |
66 | ); 67 | }; 68 | 69 | export default Bottom; 70 | -------------------------------------------------------------------------------- /components/bottom/BottomTitle.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | interface BottomTitleProps { 4 | text: string; 5 | size?: "sm" | "md" | "lg"; 6 | weight?: "light" | "medium" | "bold"; 7 | } 8 | 9 | const BottomTitle: React.FC = ({ 10 | text, 11 | size = "md", 12 | weight = "medium", 13 | }) => { 14 | return ( 15 |

26 | {text} 27 |

28 | ); 29 | }; 30 | 31 | export default BottomTitle; 32 | -------------------------------------------------------------------------------- /components/follow/UserFollowers.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from "react"; 2 | 3 | import { useRouter } from "next/router"; 4 | 5 | import { RiLoader5Line } from "react-icons/ri"; 6 | 7 | import ColorUtils from "@/base/colors"; 8 | 9 | import useFollowingDetails from "@/hooks/useFollowingDetails"; 10 | import useFollow from "@/hooks/useFollow"; 11 | import useCurrentUser from "@/hooks/useCurrentUser"; 12 | 13 | import Avatar from "@/components/Avatar"; 14 | import Button from "@/components/shared/Button"; 15 | 16 | interface IFollowersUser { 17 | username: string; 18 | } 19 | 20 | const UserFollowers: FC = ({ username }) => { 21 | const router = useRouter(); 22 | const { data: userDetails } = useFollowingDetails(username); 23 | const { data: isLoggedIn } = useCurrentUser(); 24 | const { userFollowingList, toggleFollow } = useFollow(username); 25 | 26 | return ( 27 | <> 28 | {userDetails?.followers.length > 0 ? ( 29 | userDetails.followers.map((user: any) => { 30 | const isCurrentUser = isLoggedIn.username === user.username; 31 | const isFollowing = userFollowingList.includes(user.id); 32 | return ( 33 |
37 | 38 |
{ 41 | router.push(`/users/${user.username}`); 42 | }} 43 | > 44 |

48 | {user.name} 49 |

50 |
51 | @{user.username} 52 |
53 |

54 | {user.bio} 55 |

56 |
57 |
58 | {!isCurrentUser && ( 59 |
75 |
76 | ); 77 | }) 78 | ) : ( 79 |
80 | 88 |
89 | )} 90 | 91 | ); 92 | }; 93 | 94 | export default UserFollowers; 95 | -------------------------------------------------------------------------------- /components/follow/UserFollowing.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from "react"; 2 | 3 | import { useRouter } from "next/router"; 4 | 5 | import { RiLoader5Line } from "react-icons/ri"; 6 | 7 | import ColorUtils from "@/base/colors"; 8 | 9 | import useFollowingDetails from "@/hooks/useFollowingDetails"; 10 | import useFollow from "@/hooks/useFollow"; 11 | import useCurrentUser from "@/hooks/useCurrentUser"; 12 | 13 | import Avatar from "@/components/Avatar"; 14 | import Button from "@/components/shared/Button"; 15 | 16 | interface IFollowingsUser { 17 | username: string; 18 | } 19 | 20 | const UserFollowing: FC = ({ username }) => { 21 | const router = useRouter(); 22 | const { data: userDetails } = useFollowingDetails(username); 23 | const { data: isLoggedIn } = useCurrentUser(); 24 | const { toggleFollow, userFollowingList } = useFollow(username); 25 | 26 | return ( 27 | <> 28 | {userDetails?.following.length > 0 ? ( 29 | userDetails.following.map((user: any) => { 30 | const isCurrentUser = isLoggedIn?.username === user?.username; 31 | const isFollowing = userFollowingList.includes(user.id); 32 | 33 | return ( 34 |
38 | 39 |
{ 42 | router.push(`/users/${user.username}`); 43 | }} 44 | > 45 |

49 | {user.name} 50 |

51 |
52 | @{user.username} 53 |
54 |

55 | {user.bio} 56 |

57 |
58 |
59 | {!isCurrentUser ? ( 60 |
76 |
77 | ); 78 | }) 79 | ) : ( 80 |
81 | 89 |
90 | )} 91 | 92 | ); 93 | }; 94 | 95 | export default UserFollowing; 96 | -------------------------------------------------------------------------------- /components/modals/EditModal.tsx: -------------------------------------------------------------------------------- 1 | import React, { useCallback, useEffect, useMemo, useState } from "react"; 2 | 3 | import axios from "axios"; 4 | import { toast } from "react-hot-toast"; 5 | /* import DatePicker from "react-datepicker"; 6 | import "react-datepicker/dist/react-datepicker.css"; */ 7 | /* import DatePicker from "react-date-picker"; 8 | import "react-date-picker/dist/DatePicker.css"; 9 | import "react-calendar/dist/Calendar.css"; */ 10 | import { format } from "date-fns"; 11 | 12 | import useCurrentUser from "@/hooks/useCurrentUser"; 13 | import useEditModal from "@/hooks/useEditModal"; 14 | import useUser from "@/hooks/useUser"; 15 | 16 | import ImageUpload from "../ImageUpload"; 17 | import Input from "../shared/Input"; 18 | import Modal from "../shared/Modal"; 19 | import BasicDatePicker from "../shared/BasicDatePicker"; 20 | import dayjs, { Dayjs } from "dayjs"; 21 | 22 | const EditModal = () => { 23 | const editModal = useEditModal(); 24 | 25 | const { data: currentUser } = useCurrentUser(); 26 | const { mutate: mutateUser } = useUser(currentUser?.username); 27 | 28 | const [userInfo, setUserInfo] = useState({ 29 | name: "", 30 | username: "", 31 | bio: "", 32 | profileImage: "", 33 | coverImage: "", 34 | location: "", 35 | website: "", 36 | birthday: "", 37 | }); 38 | 39 | useEffect(() => { 40 | setUserInfo({ 41 | bio: currentUser?.bio, 42 | coverImage: currentUser?.coverImage, 43 | name: currentUser?.name, 44 | profileImage: currentUser?.profileImage, 45 | username: currentUser?.username, 46 | location: currentUser?.location, 47 | website: currentUser?.website, 48 | birthday: currentUser?.birthday, 49 | }); 50 | }, [ 51 | currentUser?.bio, 52 | currentUser?.coverImage, 53 | currentUser?.name, 54 | currentUser?.profileImage, 55 | currentUser?.username, 56 | currentUser?.location, 57 | currentUser?.website, 58 | currentUser?.birthday, 59 | ]); 60 | 61 | const [isLoading, setIsLoading] = useState(false); 62 | const [formattedBirthday, setFormattedBirthday] = useState( 63 | format(new Date(), "dd MMMM yyyy") 64 | ); 65 | const [value, setValue] = React.useState(dayjs("2022-04-17")); 66 | 67 | const handleSubmit = useCallback(async () => { 68 | try { 69 | setIsLoading(true); 70 | await axios.patch("/api/edit", { 71 | name: userInfo.name, 72 | bio: userInfo.bio, 73 | username: userInfo.username, 74 | profileImage: userInfo.profileImage, 75 | coverImage: userInfo.coverImage, 76 | location: userInfo.location, 77 | website: userInfo.website, 78 | birthday: userInfo.birthday, 79 | }); 80 | 81 | mutateUser(); 82 | toast.success("Updated"); 83 | editModal.onClose(); 84 | } catch (error: any) { 85 | toast.error(error.response.data || "Something went wrong!"); 86 | } finally { 87 | setIsLoading(false); 88 | } 89 | }, [ 90 | editModal, 91 | mutateUser, 92 | userInfo.bio, 93 | userInfo.coverImage, 94 | userInfo.name, 95 | userInfo.profileImage, 96 | userInfo.username, 97 | userInfo.location, 98 | userInfo.website, 99 | userInfo.birthday, 100 | ]); 101 | 102 | const bodyContent = useMemo( 103 | () => ( 104 |
105 |
106 |
107 | 111 | setUserInfo({ ...userInfo, coverImage: image }) 112 | } 113 | /> 114 |
115 |
116 | 120 | setUserInfo({ ...userInfo, profileImage: image }) 121 | } 122 | /> 123 |
124 |
125 | 128 | setUserInfo({ ...userInfo, name: event.target.value }) 129 | } 130 | type="text" 131 | placeholder="Name" 132 | /> 133 | 136 | setUserInfo({ ...userInfo, username: event.target.value }) 137 | } 138 | type="text" 139 | placeholder="Username" 140 | disabled 141 | /> 142 | 145 | setUserInfo({ ...userInfo, bio: event.target.value }) 146 | } 147 | type="text" 148 | placeholder="Bio" 149 | multiline 150 | rows={3} 151 | /> 152 | { 155 | setUserInfo({ ...userInfo, location: event.target.value }); 156 | }} 157 | type="text" 158 | placeholder="Location" 159 | /> 160 | 163 | setUserInfo({ ...userInfo, website: event?.target?.value?.trim() }) 164 | } 165 | type="text" 166 | placeholder="Website" 167 | /> 168 | {/* { 171 | setUserInfo({ ...userInfo, birthday: event.target.value }); 172 | setFormattedBirthday( 173 | format(new Date(event.target.value), "dd MMMM yyyy") 174 | ); 175 | }} 176 | type="text" 177 | placeholder="birthday" 178 | /> */} 179 | {/* */} 183 |
184 | ), 185 | [userInfo] 186 | ); 187 | 188 | return ( 189 | 197 | ); 198 | }; 199 | 200 | export default EditModal; 201 | -------------------------------------------------------------------------------- /components/modals/LoginModal.tsx: -------------------------------------------------------------------------------- 1 | import React, { useCallback, useEffect, useState } from "react"; 2 | 3 | import { signIn } from "next-auth/react"; 4 | import { toast } from "react-hot-toast"; 5 | 6 | import ColorUtils from "@/base/colors"; 7 | 8 | import useLoginModal from "@/hooks/useLoginModal"; 9 | import useRegisterModal from "@/hooks/useRegisterModal"; 10 | 11 | import Modal from "@/components/shared/Modal"; 12 | import Input from "@/components/shared/Input"; 13 | import Loading from "@/components/shared/Loading"; 14 | 15 | const LoginModal = () => { 16 | const loginModal = useLoginModal(); 17 | const registerModal = useRegisterModal(); 18 | 19 | const [loginInput, setLoginInput] = useState(""); 20 | const [password, setPassword] = useState(""); 21 | const [loading, setLoading] = useState(false); 22 | 23 | const handleSubmit = useCallback(async () => { 24 | try { 25 | setLoading(true); 26 | 27 | const result = await signIn("credentials", { 28 | loginInput, 29 | password, 30 | }); 31 | 32 | if (result?.error === "CredentialsSignin") { 33 | toast.error("Account not found or credentials are incorrect."); 34 | } else { 35 | toast.success("Login successfully!"); 36 | loginModal.onClose(); 37 | } 38 | } catch (error: any) { 39 | toast.error("Something went wrong!" + error.message); 40 | } finally { 41 | setLoading(false); 42 | } 43 | }, [loginModal, loginInput, password]); 44 | 45 | useEffect(() => { 46 | return () => { 47 | setLoginInput(""); 48 | setPassword(""); 49 | }; 50 | }, []); 51 | 52 | if (loading) return ; 53 | 54 | const handleFooterClick = () => { 55 | loginModal.onClose(); 56 | registerModal.onOpen(); 57 | }; 58 | 59 | const bodyContent = ( 60 |
61 | setLoginInput(e.target.value)} 66 | /> 67 | setPassword(e.target.value)} 72 | /> 73 |
74 | ); 75 | const footerContent = ( 76 |

77 | Don't have an account? 78 | 83 | Sign up 84 | 85 |

86 | ); 87 | 88 | return ( 89 | 99 | ); 100 | }; 101 | 102 | export default LoginModal; 103 | -------------------------------------------------------------------------------- /components/modals/RegisterModal.tsx: -------------------------------------------------------------------------------- 1 | import { useCallback, useEffect, useState } from "react"; 2 | 3 | import axios from "axios"; 4 | import { toast } from "react-hot-toast"; 5 | import { signIn } from "next-auth/react"; 6 | 7 | import ColorUtils from "@/base/colors"; 8 | 9 | import useRegisterModal from "@/hooks/useRegisterModal"; 10 | import useLoginModal from "@/hooks/useLoginModal"; 11 | 12 | import Modal from "@/components/shared/Modal"; 13 | import Input from "@/components/shared/Input"; 14 | import Loading from "@/components/shared/Loading"; 15 | 16 | import { validateEmail } from "@/utils/helpers"; 17 | import { Router } from "next/router"; 18 | 19 | const RegisterModal = () => { 20 | const registerModal = useRegisterModal(); 21 | const loginModal = useLoginModal(); 22 | 23 | const [name, setName] = useState(""); 24 | const [email, setEmail] = useState(""); 25 | const [username, setUserName] = useState(""); 26 | const [password, setPassword] = useState(""); 27 | const [passwordConfirmed, setPasswordConfirmed] = useState(""); 28 | 29 | const [loading, setLoading] = useState(false); 30 | 31 | const [usernameError, setUsernameError] = useState(false); 32 | 33 | const clearInputs = () => { 34 | setName(""); 35 | setEmail(""); 36 | setUserName(""); 37 | setPassword(""); 38 | setPasswordConfirmed(""); 39 | }; 40 | 41 | const inputControl = () => { 42 | return ( 43 | !name || 44 | !username || 45 | !email || 46 | !password || 47 | !passwordConfirmed || 48 | !validateEmail(email) || 49 | !!usernameError 50 | ); 51 | }; 52 | 53 | const handleSubmit = useCallback(async () => { 54 | try { 55 | setLoading(true); 56 | 57 | if (passwordConfirmed.localeCompare(password) !== 0) { 58 | toast.error("Passwords doesn't match!"); 59 | return false; 60 | } 61 | 62 | await axios.post("/api/register", { 63 | email, 64 | username, 65 | name, 66 | password, 67 | }); 68 | 69 | toast.success("Account has been created successfully!", { 70 | position: "bottom-right", 71 | }); 72 | 73 | clearInputs(); 74 | 75 | loginModal.onOpen(); 76 | registerModal.onClose(); 77 | 78 | registerModal.onClose(); 79 | } catch (err: any) { 80 | toast.error(`Something went wrong! ${err.message}`, { 81 | duration: 3000, 82 | }); 83 | } finally { 84 | setLoading(false); 85 | } 86 | }, [ 87 | loginModal, 88 | registerModal, 89 | email, 90 | username, 91 | name, 92 | password, 93 | passwordConfirmed, 94 | ]); 95 | 96 | useEffect(() => { 97 | const fetchData = async () => { 98 | if (registerModal.isOpen) { 99 | try { 100 | const { data } = await axios.get(`/api/register/${username}`); 101 | 102 | const isUsernameExist = data.some( 103 | (user: { username: string }) => user.username === username 104 | ); 105 | 106 | if (data && username === data) { 107 | setUsernameError(true); 108 | } else { 109 | setUsernameError(false); 110 | } 111 | } catch (err: any) { 112 | console.error("Data fetch error", err); 113 | } 114 | window.history.pushState(null, "", "/register"); 115 | } else { 116 | window.history.pushState(null, "", "/"); 117 | } 118 | }; 119 | fetchData(); 120 | }, [username, registerModal.isOpen]); 121 | 122 | const handleFooterClick = () => { 123 | loginModal.onOpen(); 124 | registerModal.onClose(); 125 | }; 126 | 127 | const bodyContent = ( 128 |
129 | setName(e.target.value)} 134 | /> 135 | setEmail(e.target.value)} 140 | /> 141 | setUserName(e.target.value)} 146 | /> 147 | {/* 148 | TODO: Api ile kontrol edilmeli 149 | {username.length > 0 ? ( 150 |

151 | Username has to be different 152 |

153 | ) : ( 154 | "" 155 | )} */} 156 | setPassword(e.target.value)} 161 | /> 162 | setPasswordConfirmed(e.target.value)} 167 | /> 168 |
169 | ); 170 | 171 | const footerContent = ( 172 |

173 | Have you an account? 174 | 179 | Login 180 | 181 |

182 | ); 183 | 184 | if (loading) { 185 | return ; 186 | } 187 | 188 | return ( 189 | 199 | ); 200 | }; 201 | 202 | export default RegisterModal; 203 | -------------------------------------------------------------------------------- /components/modals/TweetModal.tsx: -------------------------------------------------------------------------------- 1 | import { FC, useCallback, useEffect, useState } from "react"; 2 | 3 | import axios from "axios"; 4 | import { CircularProgressbar, buildStyles } from "react-circular-progressbar"; 5 | import "react-circular-progressbar/dist/styles.css"; 6 | import { toast } from "react-hot-toast"; 7 | import { RiCloseFill } from "react-icons/ri"; 8 | 9 | import ColorUtils from "@/base/colors"; 10 | 11 | import useCurrentUser from "@/hooks/useCurrentUser"; 12 | import usePosts from "@/hooks/usePosts"; 13 | import useTweetActionModal from "@/hooks/useTweetActionModal"; 14 | 15 | import Avatar from "@/components/Avatar"; 16 | import Button from "@/components/shared/Button"; 17 | 18 | interface IPostFormProps { 19 | username?: string; 20 | } 21 | const TweetModal: FC = ({ username }) => { 22 | const tweetModal = useTweetActionModal(); 23 | const { data: currentUser, mutate: mutateUser } = useCurrentUser(); 24 | const { mutate: mutatePost } = usePosts(username as string); 25 | 26 | const [percentage, setPercentage] = useState(0); 27 | const [body, setBody] = useState(""); 28 | const [loading, setLoading] = useState(false); 29 | 30 | const handleSubmit = useCallback(async () => { 31 | try { 32 | setLoading(true); 33 | 34 | await axios.post("/api/posts", { body }); 35 | 36 | toast.success("Post created!"); 37 | 38 | setBody(""); 39 | mutatePost(); 40 | mutateUser(); 41 | tweetModal.onClose(); 42 | } catch (error: any) { 43 | console.log(error); 44 | toast.error("Something went wrong!"); 45 | } finally { 46 | setLoading(false); 47 | } 48 | }, [body, mutatePost, tweetModal, mutateUser]); 49 | 50 | useEffect(() => { 51 | const calculatePercentage = () => { 52 | const currentLength = body.length; 53 | const maxLength = 100; 54 | const calculatedPercentage = (currentLength / maxLength) * 100; 55 | 56 | setPercentage(calculatedPercentage); 57 | }; 58 | 59 | calculatePercentage(); 60 | }, [body]); 61 | 62 | const getProgressbarStyle = () => { 63 | if (body.length > 0 && body.length < 80) { 64 | return buildStyles({ 65 | rotation: 0, 66 | strokeLinecap: "butt", 67 | pathTransitionDuration: 0, 68 | trailColor: "#2F3336", 69 | pathColor: "#1D9BF0", 70 | }); 71 | } 72 | if (body.length >= 80 && body.length < 100) { 73 | return buildStyles({ 74 | rotation: 0, 75 | strokeLinecap: "butt", 76 | pathTransitionDuration: 0, 77 | textSize: "40px", 78 | textColor: "#71767b", 79 | trailColor: "#2F3336", 80 | pathColor: "#FFD400", 81 | }); 82 | } 83 | if (body.length >= 100) { 84 | return buildStyles({ 85 | rotation: 0, 86 | strokeLinecap: "butt", 87 | pathTransitionDuration: 0, 88 | textSize: "40px", 89 | textColor: "#F4212E", 90 | trailColor: "#2F3336", 91 | pathColor: "#F4212E", 92 | }); 93 | } 94 | }; 95 | 96 | const handleClose = useCallback(() => { 97 | tweetModal.onClose(); 98 | }, [tweetModal]); 99 | 100 | if (tweetModal.isOpen) return null; 101 | 102 | return ( 103 | <> 104 |
105 |
106 | {/*content*/} 107 |
108 |
109 |
{}
110 | 116 |
117 | 118 |
119 |
120 | 125 |
126 | 127 |
128 | 140 |
141 |
142 | 143 |
144 |
145 |
146 |
147 | {body.length > 0 && body.length < 80 && body.trim() ? ( 148 | 153 | ) : body.length >= 80 && body.trim() ? ( 154 | 160 | ) : null} 161 |
162 | 163 |
171 |
172 |
173 |
174 |
175 | 176 | ); 177 | }; 178 | 179 | export default TweetModal; 180 | -------------------------------------------------------------------------------- /components/notifications/NotificationFeed.tsx: -------------------------------------------------------------------------------- 1 | import { Fragment, useEffect } from "react"; 2 | 3 | import { formatDistanceToNowStrict } from "date-fns"; 4 | import { BsTwitter } from "react-icons/bs"; 5 | 6 | import useCurrentUser from "@/hooks/useCurrentUser"; 7 | import useNotifications from "@/hooks/useNotifications"; 8 | 9 | const NotificationFeed = () => { 10 | const { data: currentUser, mutate: mutateCurrentUser } = useCurrentUser(); 11 | const { data: notifications = [] } = useNotifications(currentUser?.id); 12 | 13 | useEffect(() => { 14 | mutateCurrentUser(); 15 | }, [mutateCurrentUser]); 16 | 17 | return ( 18 | <> 19 | {Array.isArray(notifications) && 20 | notifications.map((notification: Record) => { 21 | return ( 22 | 23 |
27 | 28 |
29 | 30 | 31 | {notification.body} 32 | 33 | 34 | 35 | {formatDistanceToNowStrict( 36 | new Date(notification.createdAt) 37 | )} 38 | 39 |
40 |
41 | {Array.isArray(notifications) && notification.length === 0 && ( 42 |
43 | No notifications 44 |
45 | )} 46 |
47 | ); 48 | })} 49 | 50 | ); 51 | }; 52 | 53 | export default NotificationFeed; 54 | -------------------------------------------------------------------------------- /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) => ( 11 | 12 | ))} 13 | 14 | ); 15 | }; 16 | 17 | export default CommentFeed; 18 | -------------------------------------------------------------------------------- /components/posts/CommentItem.tsx: -------------------------------------------------------------------------------- 1 | import { formatDistanceToNowStrict } from "date-fns"; 2 | import { useRouter } from "next/router"; 3 | import React, { useCallback, useEffect, useMemo } from "react"; 4 | import Avatar from "../Avatar"; 5 | 6 | interface CommentItemProps { 7 | data: Record; 8 | } 9 | 10 | const CommentItem: React.FC = ({ data }) => { 11 | const router = useRouter(); 12 | 13 | const goToUser = useCallback( 14 | (event: React.MouseEvent) => { 15 | event.stopPropagation(); 16 | 17 | router.push(`/users/${data?.user?.username}`); 18 | }, 19 | [router, data?.user?.username] 20 | ); 21 | 22 | const createdAt = useMemo(() => { 23 | if (!data?.createdAt) { 24 | return null; 25 | } 26 | 27 | return formatDistanceToNowStrict(new Date(data.createdAt)); 28 | }, [data?.createdAt]); 29 | 30 | return ( 31 |
32 |
33 | 34 |
35 |
36 |

40 | {data?.user?.name} 41 |

42 | 43 | @{data?.user?.username} 44 | 45 | · {createdAt} 46 |
47 |
{data?.body}
48 |
49 |
50 |
51 | ); 52 | }; 53 | 54 | export default CommentItem; 55 | -------------------------------------------------------------------------------- /components/posts/PostFeed.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC, useCallback, useMemo, useState } from "react"; 2 | import { useRouter } from "next/router"; 3 | 4 | import { formatDistanceToNowStrict } from "date-fns"; 5 | import { 6 | RiChat3Line, 7 | RiHeart3Line, 8 | RiHeart3Fill, 9 | RiMoreFill, 10 | RiDeleteBinLine, 11 | RiPushpin2Line, 12 | RiUserUnfollowLine, 13 | RiEditLine, 14 | } from "react-icons/ri"; 15 | 16 | import useCurrentUser from "@/hooks/useCurrentUser"; 17 | import useLoginModal from "@/hooks/useLoginModal"; 18 | 19 | import { controlLink } from "@/utils/helpers"; 20 | 21 | import Avatar from "@/components/Avatar"; 22 | import useLikes from "@/hooks/useLikes"; 23 | 24 | interface IPostFeedProps { 25 | username?: string; 26 | data: Record; 27 | } 28 | 29 | const PostFeed: FC = ({ data }) => { 30 | const [editPost, setEditPost] = useState(false); 31 | const [pin, setPin] = useState(false); 32 | 33 | const loginModal = useLoginModal(); 34 | const router = useRouter(); 35 | const { data: isLoggedIn } = useCurrentUser(); 36 | const { hasLiked, toggleLike } = useLikes({ 37 | postId: data.id, 38 | userId: isLoggedIn?.id, 39 | }); 40 | 41 | const goToUser = useCallback( 42 | (event: React.MouseEvent) => { 43 | event.stopPropagation(); 44 | 45 | router.push(`/users/${data?.user?.username}`); 46 | }, 47 | [router, data?.user?.username] 48 | ); 49 | 50 | const goToPost = useCallback( 51 | (event: React.MouseEvent) => { 52 | event.stopPropagation(); 53 | 54 | /* @ts-ignore */ 55 | if (isLoggedIn && event?.target?.id !== "external-url") { 56 | router.push(`/posts/${data?.id}`); 57 | } 58 | }, 59 | [isLoggedIn, router, data?.id] 60 | ); 61 | 62 | const onComment = useCallback( 63 | (event: React.MouseEvent) => { 64 | event.stopPropagation(); 65 | if (!isLoggedIn) { 66 | return loginModal.onOpen(); 67 | } 68 | }, 69 | [isLoggedIn, loginModal] 70 | ); 71 | 72 | const onLike = useCallback( 73 | (event: React.MouseEvent) => { 74 | event.stopPropagation(); 75 | if (!isLoggedIn) { 76 | return loginModal.onOpen(); 77 | } 78 | 79 | toggleLike(); 80 | }, 81 | [isLoggedIn, loginModal, toggleLike] 82 | ); 83 | 84 | const postEdit = useCallback( 85 | (event: React.MouseEvent) => { 86 | event.stopPropagation(); 87 | if (!isLoggedIn) { 88 | return loginModal.onOpen(); 89 | } 90 | setEditPost((prevState) => !prevState); 91 | }, 92 | [isLoggedIn, loginModal] 93 | ); 94 | 95 | const postDelete = useCallback( 96 | async (id: any) => { 97 | if (!isLoggedIn) { 98 | return loginModal.onOpen(); 99 | } 100 | }, 101 | [isLoggedIn, loginModal] 102 | ); 103 | const closePostEdit = (e: React.MouseEvent) => { 104 | const target = e.target as HTMLElement; 105 | const attributeValue = target.getAttribute("editpost-data"); 106 | if (attributeValue === "editPost") return; 107 | setEditPost(false); 108 | }; 109 | 110 | const createdAt = useMemo(() => { 111 | if (!data?.createdAt) { 112 | return null; 113 | } 114 | 115 | return formatDistanceToNowStrict(new Date(data.createdAt)); 116 | }, [data?.createdAt]); 117 | 118 | const LikeIcon = hasLiked ? RiHeart3Fill : RiHeart3Line; 119 | 120 | return ( 121 | <> 122 | {editPost && ( 123 |
closePostEdit(e)} 126 | /> 127 | )} 128 |
132 |
133 | 134 |
135 |
136 |
140 | {data.user.name} 141 |
142 |
146 | @{data.user.username} 147 |
148 | · 149 | {createdAt} 150 |
151 |

155 | {} 156 |

157 |
158 |
162 | 163 |

{data.Comment.length || 0}

164 |
165 |
169 | 174 |

{data.likedIds.length}

175 |
176 |
177 |
178 | { 181 | postEdit(e); 182 | }} 183 | /> 184 |
190 | {isLoggedIn && data?.user?.username === isLoggedIn?.username && ( 191 | <> 192 |

{ 195 | postDelete(data.id); 196 | }} 197 | > 198 | 199 | Delete 200 |

201 |

{ 204 | postDelete(data.id); 205 | }} 206 | > 207 | 208 | Edit 209 |

210 | 211 |

{ 214 | setPin((prevState) => !prevState); 215 | }} 216 | > 217 | 218 | {pin ? "Unpin from profile" : "Pin to profile"} 219 |

220 | 221 |

{}} 224 | > 225 | 226 | Change who can reply 227 |

228 | 229 | )} 230 | 231 | {isLoggedIn && data?.user?.username !== isLoggedIn?.username && ( 232 |

{}} 235 | > 236 | 237 | Unfollow 238 |

239 | )} 240 |
241 |
242 |
243 | 244 | ); 245 | }; 246 | 247 | export default PostFeed; 248 | -------------------------------------------------------------------------------- /components/posts/PostFeeds.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from "react"; 2 | import { RiLoader5Line } from "react-icons/ri"; 3 | import { useAutoAnimate } from "@formkit/auto-animate/react"; 4 | 5 | import ColorUtils from "@/base/colors"; 6 | 7 | import useCurrentUser from "@/hooks/useCurrentUser"; 8 | import usePosts from "@/hooks/usePosts"; 9 | 10 | import PostFeed from "@/components/posts/PostFeed"; 11 | 12 | interface IPostFeedsProps { 13 | userId?: string; 14 | username?: string; 15 | } 16 | 17 | const PostFeeds: FC = ({ userId, username }) => { 18 | const { data: posts = [], isLoading } = usePosts(userId as string); 19 | const { data: currentUser } = useCurrentUser(); 20 | 21 | const [parent] = useAutoAnimate({ duration: 500 }); 22 | 23 | if (isLoading) { 24 | return ( 25 |
26 | 34 |
35 | ); 36 | } 37 | 38 | if (!currentUser?.email) { 39 | return ( 40 | <> 41 |
42 |

43 | Please login to see the posts 44 |

45 |
46 | 47 | ); 48 | } 49 | 50 | return ( 51 | <> 52 |
53 | {Array.isArray(posts) && posts.length > 0 ? ( 54 | 55 | posts.map((post: Record) => ( 56 | 57 | )) 58 | 59 | ) : ( 60 |
61 |

62 | {username ? "No posts yet" : "Your followings have no posts yet"} 63 |

64 |
65 | )} 66 |
67 | 68 | ); 69 | }; 70 | 71 | export default PostFeeds; 72 | -------------------------------------------------------------------------------- /components/shared/BasicDatePicker.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from "react"; 2 | import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs"; 3 | import { LocalizationProvider } from "@mui/x-date-pickers/LocalizationProvider"; 4 | import { DatePicker } from "@mui/x-date-pickers/DatePicker"; 5 | import dayjs, { Dayjs } from "dayjs"; 6 | 7 | interface IDatepickerProps { 8 | label: string; 9 | value?: string | undefined | Dayjs | null; 10 | onChange?: (date: any) => void; 11 | defaultValue?: string | number | Date | null | undefined | Dayjs; 12 | } 13 | 14 | const BasicDatePicker: FC = ({ 15 | label, 16 | value = null, 17 | onChange = () => {}, 18 | defaultValue = null, 19 | }) => { 20 | return ( 21 | 22 | 34 | 35 | ); 36 | }; 37 | export default BasicDatePicker; 38 | -------------------------------------------------------------------------------- /components/shared/Button.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC, useState } from "react"; 2 | 3 | import { IconType } from "react-icons"; 4 | 5 | import ButtonUtils from "@/base/button"; 6 | import useWindowSize from "@/hooks/useWindowSize"; 7 | 8 | interface IButtonProps { 9 | label: string; 10 | secondary?: boolean; 11 | onClick?: () => void; 12 | fullWidth?: boolean; 13 | size?: "custom" | "sm" | "md" | "lg"; 14 | marginHorizontal?: string; 15 | marginVertical?: string; 16 | paddingVertical?: string; 17 | paddingHorizontal?: string; 18 | hoverBgColor?: string; 19 | hoverBorderColor?: string; 20 | hoverTextColor?: string; 21 | hoverOpacity?: string; 22 | bgColor?: string; 23 | color?: string; 24 | icon?: IconType; 25 | showShareButton?: boolean; 26 | disabled?: boolean; 27 | border?: string; 28 | borderColor?: string; 29 | style?: React.CSSProperties | undefined; 30 | type?: "button" | "submit" | "reset" | undefined; 31 | labelWeight?: 32 | | "light" 33 | | "normal" 34 | | "medium" 35 | | "semibold" 36 | | "bold" 37 | | "extrabold"; 38 | hoverEnabled?: boolean; 39 | labelSize?: "xs" | "sm" | "base" | "lg"; 40 | hoverText?: string; 41 | large?: boolean; 42 | btnBlack?: boolean; 43 | customWidth?: string; 44 | } 45 | 46 | const Button: FC = ({ 47 | label, 48 | secondary = false, 49 | btnBlack = false, 50 | onClick, 51 | fullWidth = false, 52 | size = "md", 53 | bgColor = null, 54 | color = null, 55 | icon: Icon, 56 | showShareButton = false, 57 | disabled, 58 | border = null, 59 | borderColor = null, 60 | type, 61 | labelWeight = null, 62 | hoverEnabled, 63 | labelSize, 64 | hoverText, 65 | marginHorizontal = null, 66 | marginVertical = null, 67 | paddingHorizontal = null, 68 | paddingVertical = null, 69 | hoverBgColor = null, 70 | hoverBorderColor = null, 71 | hoverTextColor = null, 72 | hoverOpacity = null, 73 | customWidth = null, 74 | }) => { 75 | const { width } = useWindowSize(); 76 | const [hover, setHover] = useState(false); 77 | 78 | const styles = [ 79 | ` 80 | ${fullWidth ? "w-full" : "w-fit"} 81 | rounded-3xl 82 | ${ 83 | secondary 84 | ? ButtonUtils.styles.secondary 85 | : btnBlack 86 | ? `${ButtonUtils.styles.blackBtn} ${hoverOpacity}` 87 | : ButtonUtils.styles.primary 88 | } 89 | 90 | ${ 91 | (size === "custom" && 92 | ButtonUtils.buttonSizes.customButtonStyle) || 93 | (size === "sm" && ButtonUtils.buttonSizes.smStyle) || 94 | (size === "md" && ButtonUtils.buttonSizes.mdStyle) || 95 | (size === "lg" && ButtonUtils.buttonSizes.lgStyle) 96 | } 97 | 98 | ${customWidth} 99 | ${bgColor} 100 | 101 | font-${labelWeight} 102 | ${color} 103 | 104 | transition-colors 105 | cursor-pointer 106 | 107 | ${border} 108 | ${borderColor} 109 | 110 | hover:bg-opacity-80 111 | 112 | ${paddingVertical} 113 | ${paddingHorizontal} 114 | 115 | ${marginVertical} 116 | ${marginHorizontal} 117 | 118 | outline-none 119 | active:outline-none 120 | 121 | ${disabled && "disabled:cursor-not-allowed opacity-60"} 122 | ${ 123 | disabled && 124 | secondary && 125 | "disabled:bg-gray-300 disabled:text-gray-500" 126 | } 127 | 128 | 129 | ${ 130 | hoverEnabled && 131 | ` 132 | ${hoverBgColor} 133 | ${hoverTextColor} 134 | ${hoverBorderColor} 135 | ${hoverOpacity}` 136 | } 137 | `, 138 | ].join(" "); 139 | 140 | return ( 141 | 172 | ); 173 | }; 174 | 175 | export default Button; 176 | -------------------------------------------------------------------------------- /components/shared/FollowPage.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from "react"; 2 | import { useRouter } from "next/router"; 3 | import useUser from "@/hooks/useUser"; 4 | import Header from "@/components/shared/Header"; 5 | 6 | const FollowPage = () => { 7 | const router = useRouter(); 8 | const { username } = router.query; 9 | const { data: fetchUser } = useUser(username as string); 10 | 11 | return ( 12 | <> 13 | 61 | 62 | ); 63 | }; 64 | 65 | export default FollowPage; 66 | -------------------------------------------------------------------------------- /components/shared/Header.tsx: -------------------------------------------------------------------------------- 1 | import { useRouter } from "next/router"; 2 | import React, { FC, useCallback } from "react"; 3 | 4 | import { RiArrowLeftLine } from "react-icons/ri"; 5 | 6 | import useUser from "@/hooks/useUser"; 7 | import { fetchData } from "next-auth/client/_utils"; 8 | 9 | interface IHeaderProps { 10 | showBackArrow?: boolean; 11 | label: string; 12 | labelUsername?: string; 13 | isProfilePage?: boolean; 14 | userName?: string; 15 | isFollowPage?: boolean; 16 | } 17 | 18 | const Header: FC = ({ 19 | label, 20 | labelUsername, 21 | showBackArrow = false, 22 | isProfilePage = false, 23 | userName, 24 | isFollowPage = false, 25 | }) => { 26 | const router = useRouter(); 27 | const { data: activeUser } = useUser(userName as string); 28 | 29 | const handleBackClick = useCallback(() => { 30 | router.back(); 31 | }, [router]); 32 | 33 | return ( 34 |
42 | {showBackArrow ? ( 43 | 48 | ) : null} 49 |
50 |

{label}

51 | {isProfilePage ? ( 52 |

53 | {activeUser?.userTwitCount} Tweets 54 |

55 | ) : null} 56 | {isFollowPage ? ( 57 |
58 |

@{labelUsername}

59 |
60 | ) : null} 61 |
62 |
63 | ); 64 | }; 65 | 66 | export default Header; 67 | -------------------------------------------------------------------------------- /components/shared/Input.tsx: -------------------------------------------------------------------------------- 1 | import React, { 2 | FC, 3 | HTMLInputTypeAttribute, 4 | useCallback, 5 | useMemo, 6 | useState, 7 | } from "react"; 8 | 9 | import { RiEyeLine, RiEyeOffLine } from "react-icons/ri"; 10 | 11 | import { 12 | isNullOrEmpty, 13 | isNullOrUndefined, 14 | validateEmail, 15 | } from "@/utils/helpers"; 16 | 17 | interface InputProps { 18 | type: HTMLInputTypeAttribute; 19 | placeholder?: string; 20 | disabled?: boolean; 21 | value?: string; 22 | onChange?: ( 23 | e: React.ChangeEvent 24 | ) => void; 25 | multiline?: boolean; 26 | rows?: number; 27 | } 28 | 29 | const Input: FC = ({ 30 | type, 31 | placeholder = "", 32 | disabled = false, 33 | value = "", 34 | onChange = () => {}, 35 | multiline = false, 36 | rows = 3, 37 | }) => { 38 | const [isPasswordHidden, setIsPasswordHidden] = useState(false); 39 | 40 | const onPasswordChangeVisibility = () => { 41 | setTimeout(() => { 42 | setIsPasswordHidden((current) => !current); 43 | }, 200); 44 | }; 45 | 46 | const placeholderText = "Email or Username"; 47 | 48 | const inputControl = useCallback( 49 | (type: string, value: string): string => { 50 | let borderColor: string = "focus:ring-transparent "; 51 | 52 | if (isNullOrEmpty(value) && isNullOrUndefined(value)) { 53 | borderColor = "focus:ring-red-600 border-gray-800"; 54 | return borderColor; 55 | } 56 | 57 | if ( 58 | type === "text" && 59 | value !== "" && 60 | value.includes("@") && 61 | placeholder === placeholderText 62 | ) { 63 | // for log in 64 | if (!validateEmail(value)) { 65 | borderColor = "focus:ring-red-600 border-red-600"; 66 | } 67 | } 68 | 69 | if (type === "email" && value !== "") { 70 | // for register 71 | if (!validateEmail(value)) { 72 | borderColor = "focus:ring-red-600 border-red-600"; 73 | } 74 | } 75 | 76 | return borderColor; 77 | }, 78 | [placeholder] 79 | ); 80 | 81 | const renderType = 82 | type === "password" ? (isPasswordHidden ? "text" : "password") : type; 83 | 84 | const errorControl = useMemo(() => { 85 | return ( 86 | value && 87 | !validateEmail(value) && 88 | type === "text" && 89 | value !== "" && 90 | value.includes("@") && 91 | placeholder === placeholderText 92 | ); 93 | }, [value, type, placeholder]); 94 | 95 | return ( 96 |
97 | {!multiline ? ( 98 |
99 | 117 | 125 |
126 | ) : ( 127 |
128 |