├── .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 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/.idea/inspectionProfiles/Project_Default.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
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 |
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 |

12 | 
13 | 
14 | 
15 | 
16 | 
17 |
18 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 | ## :star2: About the Project
35 |
36 |
37 |
38 |
39 |
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 |
97 | npm run dev
- Runs the app in the development mode.
98 | npm run start
- Runs the app in the production mode.
99 | npm run fresh
- Drops the database, creates a new one, and runs all migrations.
100 | npx prisma db seed
- Runs the seed file.
101 |
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 |
118 |
119 | Typescript (for type safety)
120 | Next.js (for server-side rendering)
121 | Prisma (for database access)
122 |
123 | Node.js (for running scripts)
124 |
125 | SWR (for data fetching)
126 |
127 | Hot-Toast (for toast notifications)
128 |
129 | Next-Auth (for authentication)
130 |
131 | Axios (for making HTTP requests)
132 |
133 | React-Icons (for icons)
134 |
135 | Zustand (for state management)
136 |
137 | Bcrypt (for hashing passwords)
138 |
139 | Prisma-Adapter (for next-auth configuration)
140 |
141 | Date-fns (for manipulating dates)
142 |
143 | Dropzone (for turns HTML element)
144 |
145 | React-Spinners (for a collection of loading)
146 |
147 |
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 |
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 |
68 |
69 |
76 | >
77 | ) : (
78 | <>
79 |
80 |
81 |
86 |
87 |
88 |
93 |
94 |
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 |
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 |
132 |
133 |
134 |
142 |
143 |
144 |
145 | ) : (
146 |
147 |
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 |
100}
188 | label="Tweet"
189 | onClick={handleSubmit}
190 | size="custom"
191 | labelSize="base"
192 | labelWeight="semibold"
193 | />
194 |
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 |
77 |
78 | {currentUser && (
79 |
router.push("/users/" + currentUser?.username)}
82 | >
83 |
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 |
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 |
85 |
86 |
87 | );
88 | })}
89 |
103 |
104 |
105 |
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 |
51 |
52 |
53 |
61 |
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 |
73 | )}
74 |
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 |
74 | ) : null}
75 |
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 |
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 |
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 |
114 |
115 |
116 |
117 |
118 |
119 |
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 |
100}
165 | label="Tweet"
166 | size="custom"
167 | labelSize="base"
168 | onClick={handleSubmit}
169 | />
170 |
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 | setHover(true)}
145 | onMouseLeave={() => setHover(false)}
146 | type={type}
147 | className={styles}
148 | >
149 | {Icon && }
150 | {width! <= 1024 && showShareButton && (
151 |
156 |
157 |
158 |
159 |
160 | )}
161 | {width! > 1024 && (
162 |
168 | {hover && hoverEnabled ? hoverText : label}
169 |
170 | )}
171 |
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 |
14 |
20 |
21 |
22 |
23 |
router.push(`/users/${username}/following`)}
30 | >
31 |
32 |
Following
33 | {router.asPath.includes("following") && (
34 |
38 | )}
39 |
40 |
41 |
router.push(`/users/${username}/followers`)}
48 | >
49 |
Followers
50 | {router.asPath.includes("followers") && (
51 |
55 | )}
56 |
57 |
58 |
59 |
60 |
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 |
123 | {placeholder}
124 |
125 |
126 | ) : (
127 |
128 |
139 |
143 | {placeholder}
144 |
145 |
146 | )}
147 | {errorControl ? (
148 |
149 | Please enter a valid email.
150 |
151 | ) : null}
152 | {!validateEmail(value) && type === "email" && value !== "" ? (
153 |
154 | Please enter a valid email.
155 |
156 | ) : null}
157 | {type === "password" ? (
158 |
162 | {isPasswordHidden ? (
163 |
164 | ) : (
165 |
166 | )}
167 |
168 | ) : null}
169 |
170 | );
171 | };
172 |
173 | export default React.memo(Input);
174 |
--------------------------------------------------------------------------------
/components/shared/Loading.tsx:
--------------------------------------------------------------------------------
1 | import { RiLoader5Line } from "react-icons/ri";
2 |
3 | import ColorUtils from "@/base/colors";
4 |
5 | const Loading = () => {
6 | return (
7 |
26 |
36 |
37 | );
38 | };
39 |
40 | export default Loading;
41 |
--------------------------------------------------------------------------------
/components/shared/Modal.tsx:
--------------------------------------------------------------------------------
1 | import React, { FC, useCallback, useEffect } from "react";
2 | import { RiCloseFill } from "react-icons/ri";
3 | import Button from "@/components/shared/Button";
4 |
5 | interface IModalProps {
6 | isOpen: boolean;
7 | children?: React.ReactNode;
8 | onClose: () => void;
9 | onSubmit: () => void;
10 | title?: string;
11 | body?: React.ReactElement;
12 | footer?: React.ReactElement;
13 | actionLabel: string;
14 | disabled?: boolean;
15 | }
16 |
17 | const Modal: FC = ({
18 | isOpen,
19 | children,
20 | onClose,
21 | onSubmit,
22 | title,
23 | body,
24 | footer,
25 | actionLabel,
26 | disabled = false,
27 | }) => {
28 | const handleClose = useCallback(() => {
29 | onClose();
30 | }, [onClose]);
31 |
32 | const handleSubmit = useCallback(() => {
33 | if (disabled) {
34 | return;
35 | }
36 | onSubmit();
37 | }, [disabled, onSubmit]);
38 |
39 | if (!isOpen) {
40 | return null;
41 | }
42 |
43 | const handleKeyDown = (event: React.KeyboardEvent) => {
44 | if (event.code === "Enter" && disabled === false) {
45 | onSubmit();
46 | }
47 | };
48 |
49 | return (
50 | <>
51 |
55 |
56 | {/*content*/}
57 |
58 | {/*header*/}
59 |
60 |
{title}
61 |
65 |
66 |
67 |
68 | {/*body*/}
69 |
{body}
70 | {/*footer*/}
71 |
72 |
81 | {footer}
82 |
83 |
84 |
85 |
86 | >
87 | );
88 | };
89 |
90 | export default Modal;
91 |
--------------------------------------------------------------------------------
/components/shared/Portal.tsx:
--------------------------------------------------------------------------------
1 | import exp from "constants";
2 | import { FC, useRef, useEffect, useState, ReactNode } from "react";
3 | import { createPortal } from "react-dom";
4 |
5 | interface PortalProps {
6 | children: ReactNode;
7 | classes?: any;
8 | }
9 |
10 | export const Portal: FC = ({ children, classes }) => {
11 | const ref = useRef(null);
12 | const [mounted, setMounted] = useState(false);
13 |
14 | useEffect(() => {
15 | ref.current = document.querySelector("#portal");
16 | setMounted(true);
17 | }, []);
18 |
19 | return mounted && ref.current
20 | ? createPortal(
21 | {children}
,
22 | ref.current
23 | )
24 | : null;
25 | };
26 | export default Portal;
27 |
--------------------------------------------------------------------------------
/components/users/UserHero.tsx:
--------------------------------------------------------------------------------
1 | import React, { FC, useState } from "react";
2 | import Image from "next/image";
3 |
4 | import { RiCloseFill } from "react-icons/ri";
5 |
6 | import ColorUtils from "@/base/colors";
7 |
8 | import useUser from "@/hooks/useUser";
9 |
10 | import Avatar from "../Avatar";
11 | import Portal from "@/components/shared/Portal";
12 |
13 | interface IUserHeroProps {
14 | username: string;
15 | }
16 |
17 | const UserHero: FC = ({ username }) => {
18 | const { data: fetchedUser } = useUser(username);
19 | const [modal, setModal] = useState(false);
20 | const [cover, setCover] = useState(false);
21 |
22 | const viewImage = (type: string) => {
23 | type === "cover" ? setCover(true) : setCover(false);
24 | document.getElementById("layout")?.classList.add("overflow-hidden");
25 | setModal(true);
26 | };
27 |
28 | const closeModal = (e: React.MouseEvent) => {
29 | const target = e.target as HTMLElement;
30 | const attributeValue = target.getAttribute("image-data");
31 | if (attributeValue === "image") return;
32 | setModal(false);
33 | document.getElementById("layout")?.classList.remove("overflow-hidden");
34 | };
35 |
36 | return (
37 |
38 | {modal && (
39 |
40 | {
44 | setModal(false);
45 | document
46 | .getElementById("layout")
47 | ?.classList.remove("overflow-hidden");
48 | }}
49 | />
50 | closeModal(e)}
53 | >
54 |
67 |
68 |
69 | )}
70 |
71 |
72 | {fetchedUser?.coverImage && (
73 |
viewImage("cover")}
79 | />
80 | )}
81 |
82 |
viewImage("profile")}
85 | />
86 |
87 |
88 |
89 |
90 | );
91 | };
92 |
93 | export default UserHero;
94 |
--------------------------------------------------------------------------------
/components/users/UserInfo.tsx:
--------------------------------------------------------------------------------
1 | import { FC, useMemo } from "react";
2 | import { useRouter } from "next/router";
3 | import Link from "next/link";
4 |
5 | import { format } from "date-fns";
6 | import { BsBalloon } from "react-icons/bs";
7 | import { RiCalendar2Line, RiLink, RiMapPinLine } from "react-icons/ri";
8 |
9 | import useCurrentUser from "@/hooks/useCurrentUser";
10 | import useEditModal from "@/hooks/useEditModal";
11 | import useFollow from "@/hooks/useFollow";
12 | import useUser from "@/hooks/useUser";
13 |
14 | import { controlLink } from "@/utils/helpers";
15 | import Button from "@/components/shared/Button";
16 |
17 | interface IUserInfoProps {
18 | username: string;
19 | }
20 |
21 | const UserInfo: FC
= ({ username }) => {
22 | const { data: fetchedUser } = useUser(username);
23 | const { data: currentUser } = useCurrentUser();
24 | const { userFollowingList, toggleFollow } = useFollow(username);
25 | const router = useRouter();
26 |
27 | const bioLink = fetchedUser?.bio ? controlLink(fetchedUser.bio) : "";
28 | const websiteLink = useMemo(() => {
29 | if (!fetchedUser?.website) {
30 | return null;
31 | }
32 |
33 | return fetchedUser?.website.includes("http") ||
34 | fetchedUser?.website.includes("https")
35 | ? fetchedUser?.website
36 | : `https://${fetchedUser?.website}`;
37 | }, [fetchedUser?.website]);
38 |
39 | const websiteText = useMemo(() => {
40 | if (!fetchedUser?.website) {
41 | return null;
42 | }
43 |
44 | return fetchedUser?.website.includes("http") ||
45 | fetchedUser?.website.includes("https")
46 | ? fetchedUser?.website.split("//")[1]
47 | : fetchedUser?.website;
48 | }, [fetchedUser?.website]);
49 |
50 | const editModal = useEditModal();
51 |
52 | const createdAt = useMemo(() => {
53 | if (!fetchedUser?.createdAt) {
54 | return null;
55 | }
56 |
57 | return format(new Date(fetchedUser?.createdAt), "MMMM yyyy");
58 | }, [fetchedUser?.createdAt]);
59 |
60 | const birthday = useMemo(() => {
61 | if (!fetchedUser?.birthday) {
62 | return null;
63 | }
64 |
65 | return format(new Date(fetchedUser?.birthday), "dd MMMM yyyy");
66 | }, [fetchedUser?.birthday]);
67 |
68 | const isFollowing = useMemo(() => {
69 | return userFollowingList.includes(fetchedUser.id);
70 | }, [userFollowingList, fetchedUser]);
71 |
72 | return (
73 |
74 |
75 | {currentUser?.username === username ? (
76 | editModal.onOpen()}
79 | size="md"
80 | labelSize="sm"
81 | btnBlack
82 | hoverEnabled
83 | hoverText="Edit profile"
84 | hoverBgColor="hover:bg-custom-white"
85 | hoverOpacity="hover:!bg-opacity-10"
86 | color="black"
87 | labelWeight="semibold"
88 | />
89 | ) : (
90 |
105 | )}
106 |
107 |
108 |
109 | {fetchedUser?.name}
110 |
111 |
112 | @{fetchedUser?.username}
113 |
114 |
118 |
119 |
120 |
121 |
122 |
Joined {createdAt}
123 |
124 | {fetchedUser?.location ? (
125 |
126 |
127 |
{fetchedUser?.location}
128 |
129 | ) : null}
130 |
131 | {websiteLink ? (
132 |
143 | ) : null}
144 |
145 | {fetchedUser?.birthday ? (
146 |
147 |
148 |
{birthday}
149 |
150 | ) : null}
151 |
152 |
153 |
154 |
router.push(`/users/${username}/following`)}>
155 |
156 | {fetchedUser?.followingIds?.length}{" "}
157 | following
158 |
159 |
160 |
router.push(`/users/${username}/followers`)}>
161 |
162 | {fetchedUser?.userFollowCount || 0}{" "}
163 | followers
164 |
165 |
166 |
167 |
168 |
169 | );
170 | };
171 |
172 | export default UserInfo;
173 |
--------------------------------------------------------------------------------
/cron.sh:
--------------------------------------------------------------------------------
1 | # get current branch
2 | current_branch=$(git rev-parse --abbrev-ref HEAD)
3 |
4 | # get main branch
5 | main_branch=$(git rev-parse --abbrev-ref main)
6 |
7 | # get master branch
8 | master_branch=$(git rev-parse --abbrev-ref master)
9 |
10 | # get remote
11 | remote=$(git config --get remote.origin.url)
12 |
13 | #merge main branch into master
14 | git checkout $master_branch
15 | git merge $main_branch
16 |
17 | # push to remote
18 | git push $remote $master_branch
19 |
20 | # checkout current branch
21 | git checkout $current_branch
22 |
--------------------------------------------------------------------------------
/hooks/useBottomBar.ts:
--------------------------------------------------------------------------------
1 | import { create } from "zustand";
2 |
3 | interface BottomBarState {
4 | isOpen: boolean;
5 | onOpen: () => void;
6 | onClose: () => void;
7 | }
8 |
9 | const useBottomBar = create((set) => ({
10 | isOpen: true,
11 | onClose: () => set({ isOpen: false }),
12 | onOpen: () => set({ isOpen: true }),
13 | }));
14 |
15 | export default useBottomBar;
--------------------------------------------------------------------------------
/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 IEditModalProps {
4 | isOpen: boolean;
5 | onOpen: () => void;
6 | onClose: () => void;
7 | }
8 |
9 | const useEditModal = create((set) => ({
10 | isOpen: false,
11 | onClose: () => set({ isOpen: false }),
12 | onOpen: () => set({ isOpen: true }),
13 | }));
14 |
15 | export default useEditModal;
16 |
--------------------------------------------------------------------------------
/hooks/useFollow.ts:
--------------------------------------------------------------------------------
1 | import { useCallback, useMemo } from "react";
2 |
3 | import axios from "axios";
4 | import { toast } from "react-hot-toast";
5 |
6 | import useCurrentUser from "@/hooks/useCurrentUser";
7 | import useLoginModal from "@/hooks/useLoginModal";
8 | import useUser from "@/hooks/useUser";
9 |
10 | const useFollow = (userName: string) => {
11 | const { data: currentUser, mutate: mutateCurrentUser } = useCurrentUser();
12 | const { data: fetchedUser, mutate: mutateFetchedUser } = useUser(userName);
13 |
14 | const loginModal = useLoginModal();
15 |
16 | const userFollowingList = useMemo(() => {
17 | return currentUser?.followingIds || [];
18 | }, [currentUser?.followingIds]);
19 |
20 | const toggleFollow = useCallback(async () => {
21 | if (!currentUser) {
22 | loginModal.onOpen();
23 | return;
24 | }
25 |
26 | try {
27 | let request;
28 |
29 | if (userFollowingList) {
30 | request = () =>
31 | axios.delete(`/api/follow`, {
32 | data: {
33 | username: fetchedUser?.username,
34 | },
35 | });
36 | } else {
37 | request = () =>
38 | axios.post(`/api/follow`, {
39 | username: fetchedUser?.username,
40 | });
41 | }
42 |
43 | await request();
44 |
45 | mutateCurrentUser();
46 | mutateFetchedUser();
47 | } catch (error: any) {
48 | console.log(error);
49 | toast.error("Failed to follow user");
50 | }
51 | }, [
52 | currentUser,
53 | fetchedUser?.username,
54 | userFollowingList,
55 | loginModal,
56 | mutateCurrentUser,
57 | mutateFetchedUser,
58 | ]);
59 |
60 | return {
61 | userFollowingList,
62 | toggleFollow,
63 | };
64 | };
65 |
66 | export default useFollow;
67 |
--------------------------------------------------------------------------------
/hooks/useFollowingDetails.ts:
--------------------------------------------------------------------------------
1 | import useSWR from "swr";
2 |
3 | import fetcher from "@/libs/fetcher";
4 |
5 | const useFollowingDetails = (username: string) => {
6 | const { data, error, isLoading, mutate } = useSWR(
7 | `/api/following?username=${username}`,
8 | fetcher
9 | );
10 |
11 | return {
12 | data,
13 | error,
14 | isLoading,
15 | mutate,
16 | };
17 | };
18 |
19 | export default useFollowingDetails;
20 |
--------------------------------------------------------------------------------
/hooks/useLikes.ts:
--------------------------------------------------------------------------------
1 | import { useCallback, useMemo } from "react";
2 |
3 | import axios from "axios";
4 | import toast from "react-hot-toast";
5 |
6 | import useCurrentUser from "./useCurrentUser";
7 | import useLoginModal from "./useLoginModal";
8 | import usePost from "./usePost";
9 | import usePosts from "./usePosts";
10 |
11 | const useLikes = ({ postId, userId }: { postId: string; userId: string }) => {
12 | const { data: currentUser, mutate: mutateUser } = useCurrentUser();
13 | const { data: fetchedPost, mutate: mutatePost } = usePost(postId);
14 | const { mutate: mutatePosts } = usePosts();
15 | const loginModal = useLoginModal();
16 |
17 | const hasLiked = useMemo(() => {
18 | const list = fetchedPost?.likedIds || [];
19 | return list.includes(currentUser?.id || "");
20 | }, [currentUser, fetchedPost]);
21 |
22 | const toggleLike = useCallback(async () => {
23 | if (!currentUser) {
24 | loginModal.onOpen();
25 | return;
26 | }
27 |
28 | try {
29 | let request;
30 |
31 | if (hasLiked) {
32 | request = () => axios.delete(`/api/likes`, { data: { postId } });
33 | } else {
34 | request = () => axios.post(`/api/likes`, { postId });
35 | }
36 |
37 | await request();
38 | mutatePost();
39 | mutatePosts();
40 | mutateUser();
41 |
42 | toast.success("Success");
43 | } catch (error: any) {
44 | console.log(error);
45 | toast.error("Failed");
46 | }
47 | }, [
48 | currentUser,
49 | hasLiked,
50 | loginModal,
51 | mutatePost,
52 | mutatePosts,
53 | postId,
54 | mutateUser,
55 | ]);
56 |
57 | return {
58 | hasLiked,
59 | toggleLike,
60 | };
61 | };
62 |
63 | export default useLikes;
64 |
--------------------------------------------------------------------------------
/hooks/useLoginModal.ts:
--------------------------------------------------------------------------------
1 | import { create } from "zustand";
2 |
3 | interface LoginModalState {
4 | isOpen: boolean;
5 | onOpen: () => void;
6 | onClose: () => void;
7 | }
8 |
9 | const useLoginModal = create((set) => ({
10 | isOpen: false,
11 | onClose: () => set({ isOpen: false }),
12 | onOpen: () => set({ isOpen: true }),
13 | }));
14 |
15 | export default useLoginModal;
16 |
--------------------------------------------------------------------------------
/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, mutate, isLoading } = useSWR(url, fetcher);
8 |
9 | return {
10 | data,
11 | isLoading,
12 | error,
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, isValidating } = useSWR(
7 | postId ? `/api/posts/${postId}` : null,
8 | fetcher
9 | );
10 |
11 | return {
12 | data,
13 | error,
14 | isLoading,
15 | mutate,
16 | isValidating,
17 | };
18 | };
19 |
20 | export default usePost;
21 |
--------------------------------------------------------------------------------
/hooks/usePosts.ts:
--------------------------------------------------------------------------------
1 | import useSWR from "swr";
2 |
3 | import fetcher from "@/libs/fetcher";
4 |
5 | const usePosts = (userId?: string) => {
6 | const fetchUrl = userId ? `/api/posts?userId=${userId}` : "/api/posts";
7 |
8 | const { data, error, isLoading, mutate } = useSWR(fetchUrl, fetcher);
9 |
10 | return {
11 | data,
12 | error,
13 | isLoading,
14 | mutate,
15 | };
16 | };
17 |
18 | export default usePosts;
19 |
--------------------------------------------------------------------------------
/hooks/useRegisterModal.ts:
--------------------------------------------------------------------------------
1 | import { create } from "zustand";
2 |
3 | interface RegisterModalState {
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 | export default useRegisterModal;
16 |
--------------------------------------------------------------------------------
/hooks/useSearch.ts:
--------------------------------------------------------------------------------
1 | import { useCallback } from "react";
2 | import axios from "axios";
3 |
4 | const useSearch = () => {
5 | const searchUsers = useCallback(async (username: string) => {
6 | if (username === "" || username === undefined || username === null) {
7 | return [];
8 | }
9 | try {
10 | const { data } = await axios.get(`/api/search`, {
11 | params: {
12 | username: username,
13 | },
14 | });
15 |
16 | return data.users;
17 | } catch (error: any) {
18 | console.log(error);
19 | return [];
20 | }
21 | }, []);
22 |
23 | return {
24 | searchUsers,
25 | };
26 | };
27 | export default useSearch;
28 |
--------------------------------------------------------------------------------
/hooks/useTweetActionModal.ts:
--------------------------------------------------------------------------------
1 | import { create } from "zustand";
2 |
3 | interface TweetActionBarState {
4 | isOpen: boolean;
5 | onOpen: () => void;
6 | onClose: () => void;
7 | }
8 |
9 | const useTweetActionModal = create((set) => ({
10 | isOpen: true,
11 | onClose: () => set({ isOpen: true }),
12 | onOpen: () => set({ isOpen: false }),
13 | }));
14 |
15 | export default useTweetActionModal;
16 |
--------------------------------------------------------------------------------
/hooks/useUser.ts:
--------------------------------------------------------------------------------
1 | import useSWR from "swr";
2 |
3 | import fetcher from "@/libs/fetcher";
4 |
5 | const useUser = (username: string) => {
6 | const { data, error, isLoading, mutate } = useSWR(
7 | username ? `/api/users/${username}` : null,
8 | fetcher
9 | );
10 |
11 | return {
12 | data,
13 | error,
14 | isLoading,
15 | mutate,
16 | };
17 | };
18 |
19 | export default useUser;
20 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/hooks/useWindowSize.ts:
--------------------------------------------------------------------------------
1 | import {useEffect, useState} from "react";
2 |
3 | interface IWindowSize {
4 | width: number | undefined;
5 | height: number | undefined;
6 | }
7 |
8 | const useWindowSize = () => {
9 | const [windowSize, setWindowSize] = useState({
10 | width: undefined,
11 | height: undefined,
12 | });
13 |
14 | useEffect(() => {
15 | const handleResize = () => {
16 | setWindowSize({
17 | width: window.innerWidth,
18 | height: window.innerHeight,
19 | });
20 | };
21 |
22 | window.addEventListener("resize", handleResize);
23 | handleResize();
24 |
25 | return () => window.removeEventListener("resize", handleResize);
26 | }, []);
27 |
28 |
29 | return windowSize;
30 | };
31 |
32 | export default useWindowSize;
--------------------------------------------------------------------------------
/libs/fetcher.ts:
--------------------------------------------------------------------------------
1 | import axios from "axios";
2 |
3 | const fetcher = async (url: string) => {
4 | try {
5 | const response = await axios.get(url);
6 | return response.data;
7 | } catch (error: any) {
8 | throw error.response.data;
9 | }
10 | };
11 |
12 | export default fetcher;
13 |
--------------------------------------------------------------------------------
/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") {
9 | globalThis.prisma = client;
10 | }
11 |
12 | export default client;
13 |
--------------------------------------------------------------------------------
/libs/serverAuth.ts:
--------------------------------------------------------------------------------
1 | import { NextApiRequest, NextApiResponse } from "next";
2 |
3 | import prisma from "@/libs/prismadb";
4 | import { getServerSession } from "next-auth";
5 | import { authOptions } from "@/pages/api/auth/[...nextauth]";
6 | import { exclude } from "@/utils/helpers";
7 |
8 | const serverAuth = async (req: NextApiRequest, res: NextApiResponse) => {
9 | const session = await getServerSession(req, res, authOptions);
10 |
11 | if (!session?.user?.email) {
12 | throw new Error("Not signed in");
13 | }
14 |
15 | const currentUser = await prisma.user.findUnique({
16 | where: {
17 | email: session.user.email,
18 | },
19 | include: {
20 | posts: true,
21 | comments: true,
22 | notifications: true,
23 | },
24 | });
25 |
26 | if (!currentUser) {
27 | throw new Error("Not signed in");
28 | }
29 |
30 | const userWithoutPassword = exclude(currentUser, ["hashedPassword"]);
31 | return { currentUser: userWithoutPassword };
32 | };
33 |
34 | export default serverAuth;
35 |
--------------------------------------------------------------------------------
/next.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('next').NextConfig} */
2 | const nextConfig = {
3 | reactStrictMode: true,
4 | images: {
5 | domains: ["avatars.githubusercontent.com", "images.unsplash.com"],
6 | },
7 | };
8 |
9 | module.exports = nextConfig;
10 |
--------------------------------------------------------------------------------
/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 | "dependencies": {
12 | "@emotion/react": "^11.11.0",
13 | "@emotion/styled": "^11.11.0",
14 | "@formkit/auto-animate": "^1.0.0-beta.6",
15 | "@mui/material": "^5.13.1",
16 | "@mui/styled-engine-sc": "^5.12.0",
17 | "@mui/x-date-pickers": "^6.5.0",
18 | "@next-auth/prisma-adapter": "^1.0.5",
19 | "@prisma/client": "4.12.0",
20 | "@types/lodash": "^4.14.195",
21 | "@types/node": "18.15.10",
22 | "@types/react": "18.0.30",
23 | "@types/react-datepicker": "^4.11.2",
24 | "@types/react-dom": "18.0.11",
25 | "@vercel/analytics": "^1.0.1",
26 | "axios": "^1.3.4",
27 | "date-fns": "^2.30.0",
28 | "dayjs": "^1.11.7",
29 | "eslint": "8.36.0",
30 | "eslint-config-next": "13.2.4",
31 | "loadash": "^1.0.0",
32 | "lodash": "^4.17.21",
33 | "next": "13.2.4",
34 | "next-auth": "^4.21.1",
35 | "react": "18.2.0",
36 | "react-circular-progressbar": "^2.1.0",
37 | "react-cookie": "^4.1.1",
38 | "react-dom": "18.2.0",
39 | "react-dropzone": "^14.2.3",
40 | "react-hot-toast": "^2.4.0",
41 | "react-spinners": "^0.13.8",
42 | "styled-components": "^6.0.0-rc.1",
43 | "swr": "^2.1.2",
44 | "typescript": "5.0.2",
45 | "zustand": "^4.3.6"
46 | },
47 | "devDependencies": {
48 | "@types/bcrypt": "^5.0.0",
49 | "autoprefixer": "^10.4.14",
50 | "bcrypt": "^5.1.0",
51 | "postcss": "^8.4.21",
52 | "prisma": "^4.12.0",
53 | "react-icons": "^4.8.0",
54 | "tailwind-scrollbar": "^3.0.4",
55 | "tailwindcss": "^3.2.7"
56 | },
57 | "description": "",
58 | "main": "next.config.js",
59 | "repository": {
60 | "type": "git",
61 | "url": "git+https://github.com/harundogdu/twitter-clone.git"
62 | },
63 | "keywords": [],
64 | "author": "",
65 | "license": "ISC",
66 | "bugs": {
67 | "url": "https://github.com/harundogdu/twitter-clone/issues"
68 | },
69 | "homepage": "https://github.com/harundogdu/twitter-clone#readme"
70 | }
71 |
--------------------------------------------------------------------------------
/pages/404.tsx:
--------------------------------------------------------------------------------
1 | import { useCallback } from "react";
2 | import { useRouter } from "next/router";
3 |
4 | import Button from "@/components/shared/Button";
5 |
6 | const Custom404 = () => {
7 | const router = useRouter();
8 |
9 | const onClick = useCallback(() => {
10 | router.push("/");
11 | }, [router]);
12 |
13 | return (
14 |
15 |
16 | Hmm...this page doesn't exist. Try searching for something else.
17 |
18 |
26 |
27 | );
28 | };
29 | export default Custom404;
30 |
--------------------------------------------------------------------------------
/pages/_app.tsx:
--------------------------------------------------------------------------------
1 | import "@/styles/globals.css";
2 |
3 | import React, { useEffect, useState } from "react";
4 |
5 | import type { AppProps } from "next/app";
6 | import Head from "next/head";
7 |
8 | import { useAutoAnimate } from "@formkit/auto-animate/react";
9 | import { Toaster } from "react-hot-toast";
10 | import { SessionProvider } from "next-auth/react";
11 | import { Analytics } from "@vercel/analytics/react";
12 |
13 | import Layout from "@/components/Layout";
14 | import Splash from "@/components/Splash";
15 |
16 | import LoginModal from "@/components/modals/LoginModal";
17 | import RegisterModal from "@/components/modals/RegisterModal";
18 | import Bottom from "@/components/bottom/Bottom";
19 | import EditModal from "@/components/modals/EditModal";
20 | import TweetModal from "@/components/modals/TweetModal";
21 |
22 | import useCurrentUser from "@/hooks/useCurrentUser";
23 | import { title } from "process";
24 |
25 | export default function App({ Component, pageProps }: AppProps) {
26 | const [animationParent] = useAutoAnimate();
27 | const { data: isLoggedIn } = useCurrentUser();
28 | const [pageTitle, setPageTitle] = useState
("");
29 |
30 | const user = pageProps.user;
31 | const name = user?.name;
32 |
33 | useEffect(() => {
34 | const getLocationPath = () => {
35 | return window.location.pathname.substring(1);
36 | };
37 |
38 | const locationPath = getLocationPath().split("/")[0];
39 | const usersPath = getLocationPath().split("/")[1];
40 |
41 | let title =
42 | locationPath.charAt(0).toUpperCase() + locationPath.slice(1) || "Home";
43 |
44 | if (locationPath === "users") {
45 | // TODO: pulled from backend
46 | title = usersPath;
47 | }
48 |
49 | setPageTitle(title);
50 | }, []);
51 | return (
52 | <>
53 |
54 |
55 |
60 | {pageTitle ? `${pageTitle} / Twitter ` : "Twitter"}
61 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 | {!isLoggedIn && }
78 |
79 |
80 |
81 | >
82 | );
83 | }
84 |
--------------------------------------------------------------------------------
/pages/api/auth/[...nextauth].ts:
--------------------------------------------------------------------------------
1 | import bcrypt from "bcrypt";
2 | import NextAuth from "next-auth/next";
3 | import CredentialsProvider from "next-auth/providers/credentials";
4 |
5 | import { PrismaAdapter } from "@next-auth/prisma-adapter";
6 |
7 | import prisma from "@/libs/prismadb";
8 | import { AuthOptions } from "next-auth";
9 |
10 | export const authOptions: AuthOptions = {
11 | adapter: PrismaAdapter(prisma),
12 | providers: [
13 | CredentialsProvider({
14 | name: "credentials",
15 | credentials: {
16 | loginInput: { label: "loginInput", type: "text" },
17 | password: { label: "password", type: "password" },
18 | },
19 | async authorize(credentials) {
20 | if (!credentials?.loginInput || !credentials?.password) {
21 | throw new Error("Invalid credentials");
22 | }
23 |
24 | let user;
25 |
26 | if (credentials.loginInput.includes("@")) {
27 | user = await prisma.user.findUnique({
28 | where: {
29 | email: credentials.loginInput,
30 | },
31 | });
32 | }
33 | else {
34 | user = await prisma.user.findUnique({
35 | where: {
36 | username: credentials.loginInput,
37 | },
38 | });
39 | }
40 |
41 | if (!user || !user.hashedPassword) {
42 | throw new Error("Invalid credentials");
43 | }
44 |
45 | const isCorrectPassword = await bcrypt.compare(
46 | credentials.password,
47 | user.hashedPassword
48 | );
49 |
50 | if (!isCorrectPassword) {
51 | throw new Error("Invalid credentials");
52 | }
53 |
54 | return user;
55 | },
56 | }),
57 | ],
58 | debug: process.env.NODE_ENV === "development",
59 | session: {
60 | strategy: "jwt",
61 | },
62 | jwt: {
63 | secret: process.env.NEXTAUTH_JWT_SECRET,
64 | },
65 | secret: process.env.NEXTAUTH_SECRET,
66 | };
67 |
68 | export default NextAuth(authOptions);
69 |
--------------------------------------------------------------------------------
/pages/api/comments.ts:
--------------------------------------------------------------------------------
1 | import serverAuth from "@/libs/serverAuth";
2 | import { NextApiRequest, NextApiResponse } from "next";
3 |
4 | import prisma from "@/libs/prismadb";
5 |
6 | export default async function handler(
7 | req: NextApiRequest,
8 | res: NextApiResponse
9 | ) {
10 | if (req.method !== "POST") {
11 | res.status(405).json({ message: "Method not allowed" });
12 | return;
13 | }
14 |
15 | try {
16 | const { currentUser } = await serverAuth(req, res);
17 | const { body } = req.body;
18 | const { postId } = req.query;
19 |
20 | if (!postId || typeof postId !== "string") {
21 | throw new Error("Invalid post id");
22 | }
23 |
24 | const comment = await prisma?.comment.create({
25 | data: {
26 | body,
27 | userId: currentUser?.id as string,
28 | postId: postId as string,
29 | },
30 | });
31 |
32 | try {
33 | const post = await prisma.post.findUnique({
34 | where: {
35 | id: postId,
36 | },
37 | });
38 |
39 | if (post?.userId) {
40 | await prisma.notification.create({
41 | data: {
42 | body: "Someone replied your post",
43 | userId: post.userId,
44 | },
45 | });
46 |
47 | await prisma.user.update({
48 | where: {
49 | id: post.userId,
50 | },
51 | data: {
52 | hasNotification: true,
53 | },
54 | });
55 | }
56 | } catch (error: any) {
57 | console.log(error);
58 | }
59 |
60 | return res.status(200).json(comment);
61 | } catch (error: any) {
62 | console.log(error);
63 | return res.status(500).json({ message: error.message });
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/pages/api/current.ts:
--------------------------------------------------------------------------------
1 | import serverAuth from "@/libs/serverAuth";
2 | import { NextApiRequest, NextApiResponse } from "next";
3 |
4 | export default async function handler(
5 | req: NextApiRequest,
6 | res: NextApiResponse
7 | ) {
8 | if (req.method !== "GET") {
9 | return res.status(405).end();
10 | }
11 |
12 | try {
13 | const { currentUser } = await serverAuth(req, res);
14 | return res.status(200).json(currentUser);
15 | } catch (error: any) {
16 | return res.status(401).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(
7 | req: NextApiRequest,
8 | res: NextApiResponse
9 | ) {
10 | if (req.method !== "PATCH") {
11 | return res.status(405).end();
12 | }
13 |
14 | try {
15 | const { currentUser } = await serverAuth(req, res);
16 |
17 | const {
18 | name,
19 | username,
20 | bio,
21 | profileImage,
22 | coverImage,
23 | location,
24 | website,
25 | birthday,
26 | } = req.body;
27 |
28 | if (!name || !username) {
29 | throw new Error("Missing fields");
30 | }
31 |
32 | const updatedUser = await prisma.user.update({
33 | where: {
34 | id: currentUser.id,
35 | },
36 | data: {
37 | name,
38 | username,
39 | bio,
40 | profileImage,
41 | coverImage,
42 | location,
43 | website,
44 | birthday,
45 | },
46 | });
47 |
48 | return res.status(200).json(updatedUser);
49 | } catch (error: any) {
50 | return res.status(400).end();
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/pages/api/follow.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(
7 | req: NextApiRequest,
8 | res: NextApiResponse
9 | ) {
10 | if (req.method !== "POST" && req.method !== "DELETE") {
11 | return res.status(405).end();
12 | }
13 |
14 | try {
15 | const { username } = req.body;
16 | const { currentUser } = await serverAuth(req, res);
17 |
18 | if (!username || typeof username !== "string") {
19 | throw new Error("Invalid username");
20 | }
21 |
22 | const user = await prisma.user.findUnique({
23 | where: {
24 | username,
25 | },
26 | });
27 |
28 | if (!user) {
29 | throw new Error("User not found");
30 | }
31 |
32 | let updatingFollowingIds = [...(currentUser.followingIds || [])];
33 |
34 | if (req.method === "POST") {
35 | updatingFollowingIds.push(user.id);
36 |
37 | try {
38 | await prisma.notification.create({
39 | data: {
40 | body: "Someone followed you",
41 | userId: user.id,
42 | },
43 | });
44 |
45 | await prisma.user.update({
46 | where: {
47 | id: user.id,
48 | },
49 | data: {
50 | hasNotification: true,
51 | },
52 | });
53 | } catch (error: any) {
54 | console.log(error);
55 | }
56 | }
57 |
58 | if (req.method === "DELETE") {
59 | updatingFollowingIds = updatingFollowingIds.filter(
60 | (id) => id !== user.id
61 | );
62 | }
63 |
64 | const updatedUser = await prisma.user.update({
65 | where: {
66 | id: currentUser.id,
67 | },
68 | data: {
69 | followingIds: updatingFollowingIds,
70 | },
71 | });
72 |
73 | return res.status(200).json({ user: updatedUser });
74 | } catch (error: any) {
75 | console.log(error);
76 | return res.status(500).json({ message: error.message });
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/pages/api/following.ts:
--------------------------------------------------------------------------------
1 | import { NextApiRequest, NextApiResponse } from "next";
2 |
3 | import prisma from "@/libs/prismadb";
4 |
5 | export default async function handler(
6 | req: NextApiRequest,
7 | res: NextApiResponse
8 | ) {
9 | if (
10 | req.method !== "POST" &&
11 | req.method !== "DELETE" &&
12 | req.method !== "PUT"
13 | ) {
14 | try {
15 | const { username } = req.query;
16 |
17 | if (!username || typeof username !== "string") {
18 | throw new Error("Invalid username");
19 | }
20 |
21 | const user = await prisma.user.findUnique({
22 | where: {
23 | username,
24 | },
25 | });
26 |
27 | if (!user) {
28 | throw new Error("User not found");
29 | }
30 |
31 | const followers = await prisma.user.findMany({
32 | where: {
33 | followingIds: {
34 | has: user.id,
35 | },
36 | },
37 | select: {
38 | id: true,
39 | name: true,
40 | username: true,
41 | bio: true,
42 | profileImage: true,
43 | },
44 | });
45 |
46 | const following = await prisma.user.findMany({
47 | where: {
48 | id: {
49 | in: user.followingIds,
50 | },
51 | },
52 | select: {
53 | id: true,
54 | name: true,
55 | username: true,
56 | bio: true,
57 | profileImage: true,
58 | },
59 | });
60 |
61 | return res.status(200).json({
62 | followers,
63 | following,
64 | });
65 | } catch (error: any) {
66 | console.log(error);
67 | return res.status(500).json({ message: error.message });
68 | }
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/pages/api/likes.ts:
--------------------------------------------------------------------------------
1 | import { NextApiRequest, NextApiResponse } from "next";
2 |
3 | import serverAuth from "@/libs/serverAuth";
4 |
5 | import prisma from "@/libs/prismadb";
6 |
7 | export default async function handler(
8 | req: NextApiRequest,
9 | res: NextApiResponse
10 | ) {
11 | if (req.method === "GET") {
12 | throw new Error("Method not allowed");
13 | }
14 | try {
15 | const { postId } = req.body;
16 | const { currentUser } = await serverAuth(req, res);
17 |
18 | if (!postId || typeof postId !== "string") {
19 | throw new Error("Invalid request");
20 | }
21 |
22 | const post = await prisma.post.findUnique({
23 | where: {
24 | id: postId,
25 | },
26 | });
27 |
28 | if (!post) {
29 | throw new Error("Invalid request");
30 | }
31 |
32 | let updatedLikes = [...(post?.likedIds || [])];
33 | if (updatedLikes.includes(currentUser.id)) {
34 | updatedLikes = updatedLikes.filter((id) => id !== currentUser.id);
35 | } else {
36 | updatedLikes.push(currentUser.id);
37 |
38 | try {
39 | const post = await prisma.post.findUnique({
40 | where: {
41 | id: postId,
42 | },
43 | });
44 |
45 | if (post?.userId) {
46 | await prisma.notification.create({
47 | data: {
48 | body: "Someone liked your post",
49 | userId: post.userId,
50 | },
51 | });
52 |
53 | await prisma.user.update({
54 | where: {
55 | id: post.userId,
56 | },
57 | data: {
58 | hasNotification: true,
59 | },
60 | });
61 | }
62 | } catch (error: any) {
63 | console.log(error);
64 | }
65 | }
66 |
67 | const updatedPost = await prisma.post.update({
68 | where: {
69 | id: postId,
70 | },
71 | data: {
72 | likedIds: updatedLikes,
73 | },
74 | });
75 |
76 | res.status(200).json({
77 | success: true,
78 | data: updatedPost,
79 | });
80 | } catch (e: any) {
81 | res.status(400).json({
82 | success: false,
83 | message: e.message,
84 | });
85 | }
86 | }
87 |
--------------------------------------------------------------------------------
/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(
6 | req: NextApiRequest,
7 | res: NextApiResponse
8 | ) {
9 | if (req.method !== "GET") {
10 | return res.status(405).json({ message: "Method not allowed" });
11 | }
12 |
13 | try {
14 | const { userId } = req.query;
15 |
16 | if (!userId || typeof userId !== "string") {
17 | throw new Error("Invalid user ID");
18 | }
19 |
20 | const notifications = await prisma.notification.findMany({
21 | where: {
22 | userId,
23 | },
24 | orderBy: {
25 | createdAt: "desc",
26 | },
27 | include: {
28 | user: true,
29 | },
30 | });
31 |
32 | await prisma.user.update({
33 | where: {
34 | id: userId as string,
35 | },
36 | data: {
37 | hasNotification: false,
38 | },
39 | });
40 |
41 | return res.status(200).json(notifications);
42 | } catch (error: any) {
43 | console.log(error);
44 | res.status(500).end();
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/pages/api/posts/[postId].ts:
--------------------------------------------------------------------------------
1 | import { NextApiRequest, NextApiResponse } from "next";
2 |
3 | import prismadb from "@/libs/prismadb";
4 |
5 | export default async function handler(
6 | req: NextApiRequest,
7 | res: NextApiResponse
8 | ) {
9 | if (req.method !== "GET") {
10 | res.status(405).json({ message: "Method not allowed" });
11 | return;
12 | }
13 |
14 | try {
15 | const { postId } = req.query;
16 |
17 | if (!postId || typeof postId !== "string") {
18 | res.status(400).json({ message: "Invalid post id" });
19 | return;
20 | }
21 |
22 | const post = await prismadb.post.findUnique({
23 | where: {
24 | id: postId as string,
25 | },
26 | include: {
27 | user: true,
28 | Comment: {
29 | include: {
30 | user: true,
31 | },
32 | },
33 | },
34 | });
35 |
36 | if (!post) {
37 | return res.status(404).json({ message: "Post not found" });
38 | }
39 |
40 | return res.status(200).json(post);
41 | } catch (error: any) {
42 | res.status(500).json({ message: error.message });
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/pages/api/posts/index.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(
7 | req: NextApiRequest,
8 | res: NextApiResponse
9 | ) {
10 | if (req.method !== "GET" && req.method !== "POST") {
11 | return res.status(405).end();
12 | }
13 |
14 | try {
15 | const { currentUser } = await serverAuth(req, res);
16 | switch (req.method) {
17 | case "POST":
18 | const { body } = req.body;
19 |
20 | const post = await prisma.post.create({
21 | data: {
22 | body,
23 | userId: currentUser.id,
24 | },
25 | });
26 |
27 | return res.status(201).json(post);
28 | case "GET":
29 | default:
30 | const { userId } = req.query;
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: {
41 | select: {
42 | name: true,
43 | username: true,
44 | hashedPassword: false,
45 | profileImage: true,
46 | },
47 | },
48 | Comment: true,
49 | },
50 | orderBy: {
51 | createdAt: "desc",
52 | },
53 | });
54 | } else {
55 | posts = await prisma.post.findMany({
56 | where: {
57 | userId: {
58 | in: [...(currentUser.followingIds || []), currentUser.id],
59 | },
60 | },
61 | include: {
62 | user: {
63 | select: {
64 | name: true,
65 | username: true,
66 | hashedPassword: false,
67 | profileImage: true,
68 | },
69 | },
70 | Comment: true,
71 | },
72 | orderBy: {
73 | createdAt: "desc",
74 | },
75 | });
76 | }
77 |
78 | return res.status(200).json(posts);
79 | }
80 | } catch (error: any) {
81 | return res.status(500).end();
82 | }
83 | }
84 |
--------------------------------------------------------------------------------
/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(
7 | req: NextApiRequest,
8 | res: NextApiResponse
9 | ) {
10 | if (req.method !== "POST") {
11 | return res.status(405).end();
12 | }
13 |
14 | try {
15 | const { email, username, name, password } = req.body;
16 |
17 | const hashedPassword = await bcrypt.hash(password, 12);
18 |
19 | const user = await prisma.user.create({
20 | data: {
21 | email,
22 | username,
23 | name,
24 | hashedPassword,
25 | },
26 | });
27 |
28 | return res.status(201).json({
29 | success: true,
30 | user,
31 | });
32 | } catch (error: any) {
33 | return res.status(401).json({
34 | success: false,
35 | message: "Invalid credentials",
36 | });
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/pages/api/search.ts:
--------------------------------------------------------------------------------
1 | import { NextApiRequest, NextApiResponse } from "next";
2 |
3 | import prisma from "@/libs/prismadb";
4 |
5 | export default async function handler(
6 | req: NextApiRequest,
7 | res: NextApiResponse
8 | ) {
9 | if (req.method !== "GET") {
10 | return res.status(405).end();
11 | }
12 | try {
13 | const { username } = req.query;
14 |
15 | if (!username || typeof username !== "string") {
16 | throw new Error("Invalid username or name");
17 | }
18 |
19 | const searchedUsersByUserName = await prisma.user.findMany({
20 | where: {
21 | username: {
22 | contains: username,
23 | mode: "insensitive",
24 | },
25 | },
26 | });
27 |
28 | const searchedUsersByName = await prisma.user.findMany({
29 | where: {
30 | name: {
31 | contains: username,
32 | mode: "insensitive",
33 | },
34 | },
35 | });
36 |
37 | const searchedUserList = searchedUsersByName.concat(
38 | searchedUsersByUserName
39 | );
40 |
41 | const searchedUserListUnique = searchedUserList.filter(
42 | (user, index, self) =>
43 | index ===
44 | self.findIndex((t) => t.username === user.username && t.id === user.id)
45 | );
46 |
47 | if (!searchedUserListUnique) {
48 | throw new Error("No users found");
49 | }
50 |
51 | return res.status(200).json({ users: searchedUserListUnique });
52 | } catch (error: any) {
53 | return res.status(401).end();
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/pages/api/users/[username].ts:
--------------------------------------------------------------------------------
1 | import { NextApiRequest, NextApiResponse } from "next";
2 |
3 | import prisma from "@/libs/prismadb";
4 |
5 | export default async function handler(
6 | req: NextApiRequest,
7 | res: NextApiResponse
8 | ) {
9 | if (req.method !== "GET") {
10 | return res.status(405).end();
11 | }
12 |
13 | try {
14 | const { username } = req.query;
15 |
16 | if (!username || typeof username !== "string") {
17 | throw new Error("Invalid userId!");
18 | }
19 |
20 | const user = await prisma.user.findUnique({
21 | where: {
22 | username: username,
23 | },
24 | });
25 |
26 | const userTwitCount = await prisma.post.count({
27 | where: {
28 | userId: user?.id,
29 | },
30 | });
31 |
32 | const userFollowCount = await prisma.user.count({
33 | where: {
34 | followingIds: {
35 | has: user?.id,
36 | },
37 | },
38 | });
39 |
40 | return res.status(200).json({
41 | ...user,
42 | userTwitCount,
43 | userFollowCount,
44 | });
45 | } catch (error: any) {
46 | return res.status(401).end();
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/pages/api/users/index.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(
7 | req: NextApiRequest,
8 | res: NextApiResponse
9 | ) {
10 | if (req.method !== "GET") {
11 | return res.status(405).end();
12 | }
13 |
14 | try {
15 | const { currentUser } = await serverAuth(req, res);
16 |
17 | const usersCount = await prisma.user.count();
18 | const skip = Math.floor(Math.random() * usersCount);
19 | const users = await prisma.user.findMany({
20 |
21 | where: {
22 | id: {
23 | not: currentUser!.id,
24 | },
25 | },
26 | orderBy: {
27 | createdAt: "desc",
28 | },
29 | });
30 |
31 | if (users.length < 3) {
32 | const remainingUsers = await prisma.user.findMany({
33 | take: 3 - users.length,
34 | where: {
35 | id: {
36 | not: currentUser!.id,
37 | },
38 | },
39 | orderBy: {
40 | createdAt: "desc",
41 | },
42 | });
43 |
44 | users.push(...remainingUsers);
45 | }
46 |
47 | return res.status(200).json(users);
48 | } catch (error: any) {
49 | return res.status(401).end();
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/pages/connect.tsx:
--------------------------------------------------------------------------------
1 | import { FC, useCallback, useMemo } from "react";
2 | import { useRouter } from "next/router";
3 |
4 | import { IUser } from "@/types/user.type";
5 | import Avatar from "@/components/Avatar";
6 |
7 | import { RiArrowLeftLine } from "react-icons/ri";
8 |
9 | import useUsers from "@/hooks/useUsers";
10 | import useFollow from "@/hooks/useFollow";
11 |
12 | import Button from "@/components/shared/Button";
13 |
14 | interface IHeaderProps {
15 | username: string;
16 | showBackArrow?: boolean;
17 | label: string;
18 | isProfilePage?: boolean;
19 | }
20 |
21 | const Connect: FC = ({ showBackArrow = false, username }) => {
22 | const { data: allUsers = [] } = useUsers();
23 | const { userFollowingList, toggleFollow } = useFollow(username);
24 | const router = useRouter();
25 |
26 | const handleBackClick = useCallback(() => {
27 | const referrer = document.referrer;
28 | if (referrer) {
29 | return router.back();
30 | }
31 | router.push("/");
32 | }, [router]);
33 |
34 | return (
35 | <>
36 |
39 |
44 |
45 |
Connect
46 |
47 |
48 |
49 |
50 | Sugg ested for you
51 |
52 | {allUsers.map((user: IUser) => {
53 | const isFollowing = userFollowingList.includes(user.id);
54 |
55 | return (
56 |
60 |
61 |
{
64 | router.push(`/users/${user.username}`);
65 | }}
66 | >
67 |
71 | {user.name}
72 |
73 |
74 | @{user.username}
75 |
76 |
77 | {user.bio}
78 |
79 |
80 |
81 |
92 |
93 |
94 | );
95 | })}
96 |
97 | >
98 | );
99 | };
100 |
101 | export default Connect;
102 |
--------------------------------------------------------------------------------
/pages/index.tsx:
--------------------------------------------------------------------------------
1 | import Header from "@/components/shared/Header";
2 |
3 | import PostForm from "@/components/PostForm";
4 | import PostFeeds from "@/components/posts/PostFeeds";
5 |
6 | export default function Home() {
7 | return (
8 |
9 |
10 |
11 |
12 |
13 | );
14 | }
15 |
--------------------------------------------------------------------------------
/pages/notifications.tsx:
--------------------------------------------------------------------------------
1 | import { NextPageContext } from "next";
2 | import { getSession } from "next-auth/react";
3 |
4 | import Header from "@/components/shared/Header";
5 | import NotificationFeed from "@/components/notifications/NotificationFeed";
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;
36 |
--------------------------------------------------------------------------------
/pages/posts/[postId].tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { useRouter } from "next/router";
3 |
4 | import { ClipLoader } from "react-spinners";
5 |
6 | import PostForm from "@/components/PostForm";
7 | import PostFeed from "@/components/posts/PostFeed";
8 | import Header from "@/components/shared/Header";
9 | import CommentFeed from "@/components/posts/CommentFeed";
10 |
11 | import usePost from "@/hooks/usePost";
12 |
13 | const PostDetail = () => {
14 | const router = useRouter();
15 | const { postId } = router.query;
16 |
17 | const { data: postData, isLoading } = usePost(postId as string);
18 |
19 | if (isLoading || !postData) {
20 | return (
21 |
22 |
23 |
24 | );
25 | }
26 |
27 | return (
28 |
29 |
30 |
31 |
36 |
37 |
38 | );
39 | };
40 |
41 | export default PostDetail;
42 |
--------------------------------------------------------------------------------
/pages/users/[username]/followers.tsx:
--------------------------------------------------------------------------------
1 | /* eslint-disable @next/next/no-img-element */
2 | import React, { useEffect, useState } from "react";
3 |
4 | import { useRouter } from "next/router";
5 |
6 | import useFollowingDetails from "@/hooks/useFollowingDetails";
7 | import useUser from "@/hooks/useUser";
8 |
9 | import UserFollowers from "@/components/follow/UserFollowers";
10 | import FollowPage from "@/components/shared/FollowPage";
11 |
12 | const Followers: React.FunctionComponent = () => {
13 | const router = useRouter();
14 | const { username } = router.query;
15 | const { isLoading } = useUser(username as string);
16 | const { data: userDetails } = useFollowingDetails(username as string);
17 |
18 | if (userDetails?.followers.length === 0) {
19 | return (
20 | <>
21 |
22 |
23 |
24 |
28 |
29 |
30 | Looking for followers?
31 |
32 |
33 | When someone follows this account, they’ll show up here. Tweeting
34 | and interacting with others helps boost followers.
35 |
36 |
37 | >
38 | );
39 | }
40 |
41 | return (
42 | <>
43 |
44 |
45 | >
46 | );
47 | };
48 |
49 | export default Followers;
50 |
--------------------------------------------------------------------------------
/pages/users/[username]/following.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from "react";
2 |
3 | import { useRouter } from "next/router";
4 |
5 | import useFollowingDetails from "@/hooks/useFollowingDetails";
6 |
7 | import UserFollowing from "@/components/follow/UserFollowing";
8 | import FollowPage from "@/components/shared/FollowPage";
9 |
10 | const Following: React.FunctionComponent = () => {
11 | const router = useRouter();
12 | const { username } = router.query;
13 | const { data: userDetails } = useFollowingDetails(username as string);
14 |
15 | if (userDetails?.following.length === 0) {
16 | return (
17 | <>
18 |
19 |
20 |
21 |
22 |
26 |
27 |
28 | @{username} isn’t following anyone
29 |
30 |
31 | Once they follow accounts, they’ll show up here.
32 |
33 |
34 | >
35 | );
36 | }
37 |
38 | return (
39 | <>
40 |
41 |
42 | >
43 | );
44 | };
45 |
46 | export default Following;
47 |
--------------------------------------------------------------------------------
/pages/users/[username]/index.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | import { useRouter } from "next/router";
4 |
5 | import ClipLoader from "react-spinners/ClipLoader";
6 |
7 | import useUser from "@/hooks/useUser";
8 |
9 | import Header from "@/components/shared/Header";
10 | import UserHero from "@/components/users/UserHero";
11 | import UserInfo from "@/components/users/UserInfo";
12 | import PostFeeds from "@/components/posts/PostFeeds";
13 |
14 | const UserView = () => {
15 | const router = useRouter();
16 | const { username } = router.query;
17 | const { data: fetchedUser, isLoading } = useUser(username as string);
18 |
19 | if (isLoading || !fetchedUser) {
20 | return (
21 |
22 |
23 |
24 | );
25 | }
26 |
27 | return (
28 |
39 | );
40 | };
41 |
42 | export default UserView;
43 |
--------------------------------------------------------------------------------
/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 | location String?
29 | website String?
30 | birthday DateTime?
31 |
32 | posts Post[]
33 | comments Comment[]
34 | notifications Notification[]
35 | }
36 |
37 | model Post {
38 | id String @id @default(auto()) @map("_id") @db.ObjectId
39 | body String
40 | createdAt DateTime @default(now())
41 | updatedAt DateTime @updatedAt
42 | userId String @db.ObjectId
43 | likedIds String[] @db.ObjectId
44 |
45 | user User @relation(fields: [userId], references: [id], onDelete: Cascade)
46 | Comment Comment[]
47 | }
48 |
49 | model Comment {
50 | id String @id @default(auto()) @map("_id") @db.ObjectId
51 | body String
52 | createdAt DateTime @default(now())
53 | updatedAt DateTime @updatedAt
54 | userId String @db.ObjectId
55 | postId String @db.ObjectId
56 |
57 | post Post @relation(fields: [postId], references: [id], onDelete: Cascade)
58 | user User @relation(fields: [userId], references: [id], onDelete: Cascade)
59 | }
60 |
61 | model Notification {
62 | id String @id @default(auto()) @map("_id") @db.ObjectId
63 | body String
64 | createdAt DateTime @default(now())
65 | userId String @db.ObjectId
66 |
67 | user User @relation(fields: [userId], references: [id], onDelete: Cascade)
68 | }
69 |
--------------------------------------------------------------------------------
/public/change-image.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/close.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/default_doge_coin.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/harundogdu/twitter-clone/bef848a958e8fd26856149abfa7980d071577219/public/default_doge_coin.png
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/harundogdu/twitter-clone/bef848a958e8fd26856149abfa7980d071577219/public/favicon.ico
--------------------------------------------------------------------------------
/public/new-twitter-favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/harundogdu/twitter-clone/bef848a958e8fd26856149abfa7980d071577219/public/new-twitter-favicon.ico
--------------------------------------------------------------------------------
/public/twitter-favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/harundogdu/twitter-clone/bef848a958e8fd26856149abfa7980d071577219/public/twitter-favicon.ico
--------------------------------------------------------------------------------
/public/twitter-user-avatar.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/harundogdu/twitter-clone/bef848a958e8fd26856149abfa7980d071577219/public/twitter-user-avatar.jpg
--------------------------------------------------------------------------------
/styles/globals.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | body {
6 | @apply overflow-y-hidden
7 | }
--------------------------------------------------------------------------------
/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 | "./base/**/*.{js,ts,jsx,tsx}",
8 | ],
9 | theme: {
10 | extend: {
11 | boxShadow: {
12 | custom: "rgba(255, 255, 255, 0.2) 0px 0px 15px",
13 | customSecondary: "rgba(255, 255, 255, 0.30) 0px 0px 3px 1px",
14 | },
15 | colors: {
16 | primary: {
17 | main: "#1f9bf0",
18 | },
19 | custom: {
20 | blue: "#1f9bf0",
21 | green: "#05b97c",
22 | pink: "#f9197f",
23 | yellow: "#ffd401",
24 | purple: "#7956ff",
25 | orange: "#ff7a00",
26 | externalRed: "#ff0000",
27 | red: "#ff0000",
28 | redHover: "rgba(255, 0, 0, 0.2)",
29 | white: "#ffffff",
30 | black: "#000000",
31 | lightBlack: "#16181c",
32 | grayMain: "#f2f2f2",
33 | darkGray: "#333333",
34 | lightGray: "#999999",
35 | gray: {
36 | main: "#71767B",
37 | },
38 | },
39 | },
40 | spacing: {
41 | small: 42,
42 | medium: 64,
43 | large: 128,
44 | },
45 | scrollbar: ["rounded-md"],
46 | borderWidth: {
47 | sm: "1px",
48 | },
49 | },
50 | },
51 |
52 | plugins: [require("tailwind-scrollbar")({ nocompatible: true })],
53 | };
54 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es2015",
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 | "paths": {
18 | "@/*": ["./*"]
19 | }
20 | },
21 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
22 | "exclude": ["node_modules"]
23 | }
24 |
--------------------------------------------------------------------------------
/types/sidebar.type.ts:
--------------------------------------------------------------------------------
1 | import { IconType } from "react-icons";
2 |
3 | export interface ISidebarType {
4 | label: string;
5 | href: string;
6 | icon: IconType;
7 | secondaryIcon?: IconType;
8 | onClick?: () => void;
9 | public?: boolean;
10 | alert?: boolean;
11 | active?: boolean;
12 | }
13 |
--------------------------------------------------------------------------------
/types/user.type.ts:
--------------------------------------------------------------------------------
1 | export interface IUser {
2 | id: string;
3 | name: string;
4 | username: string;
5 | bio: string;
6 | email: string;
7 | emailVerified: Date;
8 | image: string;
9 | coverImage: string;
10 | profileImage: string;
11 | hashedPassword: string;
12 | createdAt: Date;
13 | updatedAt: Date;
14 | followingIds: string[];
15 | hasNotification: boolean;
16 | location: String;
17 | website: String;
18 | birthday: Date;
19 |
20 | posts: IPost[];
21 |
22 | comments: IComment[];
23 | notifications: INotification[];
24 | }
25 |
26 | export interface IPost {
27 | id: string;
28 | username: string;
29 | body: string;
30 | createdAt: Date;
31 | updatedAt: Date;
32 | userId: string;
33 |
34 | comment: IComment[];
35 | }
36 |
37 | export interface IComment {
38 | id: string;
39 | username: string;
40 | body: string;
41 | createdAt: Date;
42 | updatedAt: Date;
43 | userId: string;
44 | postId: string;
45 | }
46 |
47 | export interface INotification {
48 | id: string;
49 | username: string;
50 | notificationBody: string;
51 | createdAt: Date;
52 | userId: string;
53 | }
54 |
--------------------------------------------------------------------------------
/utils/@fake.db.ts:
--------------------------------------------------------------------------------
1 | import { ISidebarType } from "@/types/sidebar.type";
2 | import { signOut } from "next-auth/react";
3 | import { CiCircleMore } from "react-icons/ci";
4 | import {
5 | RiBookmarkLine,
6 | RiHashtag,
7 | RiHome7Fill,
8 | RiLogoutBoxLine,
9 | RiMailLine,
10 | RiNotification3Line,
11 | RiSearchLine,
12 | RiUserLine,
13 | } from "react-icons/ri";
14 |
15 | const SidebarItems: ISidebarType[] = [
16 | {
17 | label: "Home",
18 | href: "/",
19 | icon: RiHome7Fill,
20 | public: true,
21 | active: true,
22 | },
23 | {
24 | label: "Explore",
25 | href: "/explore",
26 | icon: RiHashtag,
27 | secondaryIcon: RiSearchLine,
28 | public: true,
29 | active: false,
30 | },
31 | {
32 | label: "Notifications",
33 | href: "/notifications",
34 | icon: RiNotification3Line,
35 | public: false,
36 | active: true,
37 | },
38 | {
39 | label: "Messages",
40 | href: "/messages",
41 | icon: RiMailLine,
42 | public: false,
43 | active: false,
44 | },
45 | {
46 | label: "Bookmarks",
47 | href: "/bookmarks",
48 | icon: RiBookmarkLine,
49 | public: false,
50 | active: false,
51 | },
52 | {
53 | label: "Profile",
54 | href: "/users/",
55 | icon: RiUserLine,
56 | public: false,
57 | active: true,
58 | },
59 | {
60 | label: "Logout",
61 | href: "/logout",
62 | icon: RiLogoutBoxLine,
63 | public: false,
64 | onClick: signOut,
65 | active: true,
66 | },
67 | {
68 | label: "More",
69 | href: "/more",
70 | icon: CiCircleMore,
71 | public: true,
72 | active: false,
73 | },
74 | ];
75 |
76 | export { SidebarItems };
77 |
--------------------------------------------------------------------------------
/utils/constants.ts:
--------------------------------------------------------------------------------
1 | const URL_REGEX =
2 | /(?:https?:\/\/)?(?:www\.)?([a-zA-Z0-9-]+)\.[a-zA-Z0-9]+(?:\.[a-zA-Z0-9]+)?(\/[a-zA-Z0-9\W\w]*)/;
3 |
4 | const YOUTUBE_URL_REGEX =
5 | /(https?:\/\/)?(www\.)?(youtube\.com\/watch\?v=|youtu\.be\/)([a-zA-Z0-9_-]{11})/;
6 |
7 | const USER_REGEX = /@(\w+)/g;
8 |
9 | const REGEX = {
10 | URL_REGEX,
11 | USER_REGEX,
12 | YOUTUBE_URL_REGEX,
13 | };
14 |
15 | export { REGEX };
16 |
--------------------------------------------------------------------------------
/utils/enums.ts:
--------------------------------------------------------------------------------
1 | const AvatarSize = {
2 | small: 42,
3 | medium: 64,
4 | large: 128,
5 | };
6 |
7 | export { AvatarSize };
8 |
--------------------------------------------------------------------------------
/utils/helpers.ts:
--------------------------------------------------------------------------------
1 | import router from "next/router";
2 |
3 | import { REGEX } from "@/utils/constants";
4 |
5 | const validateEmail = (email: string): boolean => {
6 | const emailRegex: RegExp = /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i;
7 | return emailRegex.test(email);
8 | };
9 |
10 | const clearProtocol = (url: string): string => {
11 | return url.replace(/(^\w+:|^)\/\//, "").replace("www.", "");
12 | };
13 |
14 | const getURLText = (url: string) => {
15 | if (url.includes("youtu.be")) {
16 | url = url.replace("youtu.be/", "https://youtube.com/watch?v=");
17 | }
18 |
19 | try {
20 | const match = url.match(REGEX.URL_REGEX);
21 | if (!match) return false;
22 | const website = match[0];
23 | if (!website) return false;
24 | return website;
25 | } catch {
26 | return false;
27 | }
28 | };
29 |
30 | const handleURL = (url: string) => {
31 | if (!url) return;
32 | if (
33 | url.includes("https") ||
34 | url.includes("http") ||
35 | url.includes("www") ||
36 | REGEX.URL_REGEX.test(url) ||
37 | REGEX.YOUTUBE_URL_REGEX.test(url)
38 | ) {
39 | if (!getURLText(url)) return;
40 | if (url.includes("youtu.be")) {
41 | return url.replace("youtu.be/", "youtube.com/watch?v=");
42 | }
43 | const newURL = getURLText(url);
44 | if (!newURL) return;
45 | return newURL;
46 | }
47 | return `https://${url}`;
48 | };
49 |
50 | const controlLink = (text: string): string => {
51 | let formattedText = text;
52 |
53 | if (REGEX.USER_REGEX.test(text)) {
54 | formattedText = formattedText.replace(
55 | REGEX.USER_REGEX,
56 | `@$1 `
57 | );
58 | }
59 |
60 | if (handleURL(text)) {
61 | const match = text.match(REGEX.URL_REGEX);
62 | const allMatches = match?.[0].split(" ");
63 |
64 | let urlText = "";
65 | allMatches?.forEach((match) => {
66 | let innerFinalURL = match;
67 | if (!match.includes("http") && !match.includes("https")) {
68 | innerFinalURL = `https://${match}`;
69 | }
70 |
71 | urlText += innerFinalURL + " ";
72 | });
73 |
74 | formattedText = formattedText.replace(
75 | REGEX.URL_REGEX,
76 | urlText
77 | .split(" ")
78 | .map((text) => {
79 | return `${clearProtocol(
80 | text
81 | )} `;
82 | })
83 | .filter((text, i, arr) => arr.indexOf(text) === i)
84 | .join(" ")
85 | );
86 |
87 | return formattedText;
88 | }
89 |
90 | document.addEventListener("click", (event) => {
91 | const target = event.target as HTMLElement;
92 | if (target.classList.contains("user-link")) {
93 | const username = target.getAttribute("data-username");
94 | if (username) {
95 | onClick(username);
96 | }
97 | }
98 | });
99 |
100 | const onClick = (username: string) => {
101 | const url = `/users/${username}`;
102 | router.push(url);
103 | };
104 |
105 | return formattedText;
106 | };
107 |
108 | const isNullOrUndefined = (value: any) => {
109 | return value === null || value === undefined;
110 | };
111 |
112 | const isNullOrEmpty = (value: any) => {
113 | return value === null || value === "" || value === undefined;
114 | };
115 |
116 | function exclude(
117 | user: User,
118 | keys: Key[]
119 | ): Omit {
120 | for (let key of keys) {
121 | delete user[key];
122 | }
123 | return user;
124 | }
125 |
126 | export {
127 | isNullOrUndefined,
128 | isNullOrEmpty,
129 | validateEmail,
130 | controlLink,
131 | exclude,
132 | };
133 |
--------------------------------------------------------------------------------