├── src
├── react-app-env.d.ts
├── assets
│ ├── appstore.png
│ ├── google.png
│ ├── googleplay.png
│ ├── empty-profile.png
│ ├── whatsapp logo.png
│ ├── whatsapplogo2.png
│ └── whatsappscreen.png
├── index.css
├── index.tsx
├── App.tsx
├── core
│ ├── firebaseConfig.ts
│ └── types.ts
├── components
│ ├── Profile.tsx
│ ├── ChatHeader.tsx
│ ├── Message.tsx
│ ├── MessageView.tsx
│ ├── MessageForm.tsx
│ ├── AddUser.tsx
│ ├── RightSide.tsx
│ └── LeftSide.tsx
└── pages
│ ├── Home.tsx
│ ├── Login.tsx
│ └── Signup.tsx
├── public
├── robots.txt
├── favicon.ico
├── logo192.png
├── logo512.png
├── manifest.json
└── index.html
├── postcss.config.js
├── tailwind.config.js
├── .gitignore
├── tsconfig.json
├── README.md
└── package.json
/src/react-app-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 | Disallow:
4 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/IslemMedjahdi/whatsapp-v2-clone/HEAD/public/favicon.ico
--------------------------------------------------------------------------------
/public/logo192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/IslemMedjahdi/whatsapp-v2-clone/HEAD/public/logo192.png
--------------------------------------------------------------------------------
/public/logo512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/IslemMedjahdi/whatsapp-v2-clone/HEAD/public/logo512.png
--------------------------------------------------------------------------------
/src/assets/appstore.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/IslemMedjahdi/whatsapp-v2-clone/HEAD/src/assets/appstore.png
--------------------------------------------------------------------------------
/src/assets/google.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/IslemMedjahdi/whatsapp-v2-clone/HEAD/src/assets/google.png
--------------------------------------------------------------------------------
/src/assets/googleplay.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/IslemMedjahdi/whatsapp-v2-clone/HEAD/src/assets/googleplay.png
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | }
7 |
--------------------------------------------------------------------------------
/src/assets/empty-profile.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/IslemMedjahdi/whatsapp-v2-clone/HEAD/src/assets/empty-profile.png
--------------------------------------------------------------------------------
/src/assets/whatsapp logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/IslemMedjahdi/whatsapp-v2-clone/HEAD/src/assets/whatsapp logo.png
--------------------------------------------------------------------------------
/src/assets/whatsapplogo2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/IslemMedjahdi/whatsapp-v2-clone/HEAD/src/assets/whatsapplogo2.png
--------------------------------------------------------------------------------
/src/assets/whatsappscreen.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/IslemMedjahdi/whatsapp-v2-clone/HEAD/src/assets/whatsappscreen.png
--------------------------------------------------------------------------------
/src/index.css:
--------------------------------------------------------------------------------
1 | @import url("https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@300;400;500;600;700&display=swap");
2 |
3 | @tailwind base;
4 | @tailwind components;
5 | @tailwind utilities;
6 |
--------------------------------------------------------------------------------
/tailwind.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | content: ["./src/**/*.{js,jsx,ts,tsx}"],
3 | theme: {
4 | extend: {
5 | fontFamily: {
6 | jakarta: ["'Plus Jakarta Sans'", "sans-serif"],
7 | },
8 | colors: {
9 | green: "#128C7E",
10 | greenlight: "#D7F8F4",
11 | },
12 | },
13 | },
14 | plugins: [require("tailwind-scrollbar")],
15 | };
16 |
--------------------------------------------------------------------------------
/.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 | # production
12 | /build
13 |
14 | # misc
15 | .DS_Store
16 | .env.local
17 | .env.development.local
18 | .env.test.local
19 | .env.production.local
20 | .env
21 | npm-debug.log*
22 | yarn-debug.log*
23 | yarn-error.log*
24 |
25 | .vercel
26 |
--------------------------------------------------------------------------------
/src/index.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import ReactDOM from "react-dom/client";
3 | import "./index.css";
4 | import App from "./App";
5 | import { BrowserRouter } from "react-router-dom";
6 |
7 | const root = ReactDOM.createRoot(
8 | document.getElementById("root") as HTMLElement
9 | );
10 | root.render(
11 |
12 |
13 |
14 |
15 |
16 | );
17 |
--------------------------------------------------------------------------------
/src/App.tsx:
--------------------------------------------------------------------------------
1 | import { Route, Routes } from "react-router-dom";
2 |
3 | // pages
4 | import Home from "./pages/Home";
5 | import Login from "./pages/Login";
6 | import Signup from "./pages/Signup";
7 |
8 | function App() {
9 | return (
10 |
11 | } />
12 | } />
13 | } />
14 |
15 | );
16 | }
17 |
18 | export default App;
19 |
--------------------------------------------------------------------------------
/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | },
10 | {
11 | "src": "logo192.png",
12 | "type": "image/png",
13 | "sizes": "192x192"
14 | },
15 | {
16 | "src": "logo512.png",
17 | "type": "image/png",
18 | "sizes": "512x512"
19 | }
20 | ],
21 | "start_url": ".",
22 | "display": "standalone",
23 | "theme_color": "#000000",
24 | "background_color": "#ffffff"
25 | }
26 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "lib": [
5 | "dom",
6 | "dom.iterable",
7 | "esnext"
8 | ],
9 | "allowJs": true,
10 | "skipLibCheck": true,
11 | "esModuleInterop": true,
12 | "allowSyntheticDefaultImports": true,
13 | "strict": true,
14 | "forceConsistentCasingInFileNames": true,
15 | "noFallthroughCasesInSwitch": true,
16 | "module": "esnext",
17 | "moduleResolution": "node",
18 | "resolveJsonModule": true,
19 | "isolatedModules": true,
20 | "noEmit": true,
21 | "jsx": "react-jsx"
22 | },
23 | "include": [
24 | "src"
25 | ]
26 | }
27 |
--------------------------------------------------------------------------------
/src/core/firebaseConfig.ts:
--------------------------------------------------------------------------------
1 | import { initializeApp } from "firebase/app";
2 | import { getAuth } from "firebase/auth";
3 | import { getFirestore } from "firebase/firestore";
4 |
5 | const firebaseConfig = {
6 | apiKey: process.env.REACT_APP_FIREBASE_API_KEY,
7 | authDomain: process.env.REACT_APP_FIREBASE_AUTH_DOMAIN,
8 | projectId: process.env.REACT_APP_FIREBASE_PROJECT_ID,
9 | storageBucket: process.env.REACT_APP_FIREBASE_STORAGE_BUCKET,
10 | messagingSenderId: process.env.REACT_APP_FIREBASE_SENDER_ID,
11 | appId: process.env.REACT_APP_FIREBASE_APP_ID,
12 | };
13 |
14 | // Initialize Firebase
15 | const app = initializeApp(firebaseConfig);
16 | export const auth = getAuth(app);
17 | export const db = getFirestore(app);
18 |
--------------------------------------------------------------------------------
/src/core/types.ts:
--------------------------------------------------------------------------------
1 | import { Timestamp } from "firebase/firestore";
2 |
3 | export type User = {
4 | uid: string;
5 | email: string | null;
6 | birthday: BirthdayDate;
7 | fName: string;
8 | lName: string;
9 | picture: string;
10 | };
11 | export type BirthdayDate = {
12 | day: number;
13 | month: number;
14 | year: number;
15 | };
16 |
17 | export type SignUpType = {
18 | fName: string;
19 | lName: string;
20 | email: string;
21 | birthday: BirthdayDate;
22 | password: string;
23 | confirmPassword: string;
24 | };
25 |
26 | export type LoginType = {
27 | email: string;
28 | password: string;
29 | };
30 |
31 | export type chatRoom = {
32 | createdAt: Timestamp;
33 | id: string;
34 | updatedAt: Timestamp;
35 | userIds: [string, string];
36 | lastMessage: string;
37 | };
38 |
39 | export type Message = {
40 | message: string;
41 | messageId: string;
42 | sender: string;
43 | type: string;
44 | createdAt: Timestamp;
45 | };
46 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Whatsapp-v2-clone
2 |
3 | ### Live Preview :
4 | **[whatsapp v1 clone](https://whatsapp-v2-clone.vercel.app/)**
5 |
6 |
7 |
8 |
9 |
10 | ### Built with:
11 |
12 | - ReactJs Typescript
13 | - TailwindCss
14 | - Framer Motion
15 | - Firebase
16 |
17 | ### Make sure you have:
18 |
19 | - Git
20 | - Nodejs version 14 or higher (we recommend using nvm)
21 | - yarn ( or npm )
22 |
23 | ### Run it localy:
24 |
25 | - Open terminal and clone the repo :
26 | git clone https://github.com/IslemMedjahdi/whatsapp-v2-clone/
27 | - then :
28 | yarn install
29 | - then :
30 | create a .env file in the root folder then put your firebase configuration ( check firebase documentation to get started )
31 | REACT_APP_FIREBASE_API_KEY =
32 | REACT_APP_FIREBASE_AUTH_DOMAIN =
33 | REACT_APP_FIREBASE_PROJECT_ID =
34 | REACT_APP_FIREBASE_STORAGE_BUCKET =
35 | REACT_APP_FIREBASE_MESSAGING_SENDER_ID =
36 | REACT_APP_FIREBASE_APP_ID =
37 | - then :
38 | yarn start
39 |
--------------------------------------------------------------------------------
/src/components/Profile.tsx:
--------------------------------------------------------------------------------
1 | import whatsappLogo from "../assets/whatsapp logo.png";
2 |
3 | type Props = {
4 | setOpen: (b: boolean) => void;
5 | };
6 |
7 | export default function Profile({ setOpen }: Props) {
8 | return (
9 |
10 |
11 |
setOpen(true)}
13 | className="cursor-pointer mr-5 md:hidden"
14 | >
15 |
27 |
28 |
29 |

34 |
35 |
WhatsApp
36 |
37 |
38 |
39 | );
40 | }
41 |
--------------------------------------------------------------------------------
/src/components/ChatHeader.tsx:
--------------------------------------------------------------------------------
1 | type Props = {
2 | setOpen: (b: boolean) => void;
3 | email: string;
4 | lName: string;
5 | fName: string;
6 | picture: string;
7 | };
8 |
9 | export default function ChatHeader({
10 | setOpen,
11 | email,
12 | lName,
13 | fName,
14 | picture,
15 | }: Props) {
16 | return (
17 |
18 |
setOpen(true)}
20 | className="cursor-pointer mr-5 md:hidden"
21 | >
22 |
34 |
35 |
36 |

41 |
42 |
43 |
{fName + " " + lName}
44 |
{email}
45 |
46 |
47 | );
48 | }
49 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "whatsapp-v2-clone",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "@hassanmojab/react-modern-calendar-datepicker": "^3.1.7",
7 | "@testing-library/jest-dom": "^5.14.1",
8 | "@testing-library/react": "^13.0.0",
9 | "@testing-library/user-event": "^13.2.1",
10 | "@types/jest": "^27.0.1",
11 | "@types/node": "^16.7.13",
12 | "@types/react": "^18.0.0",
13 | "@types/react-dom": "^18.0.0",
14 | "emoji-picker-react": "^3.5.1",
15 | "firebase": "^9.8.1",
16 | "framer-motion": "^6.3.3",
17 | "react": "^18.1.0",
18 | "react-dom": "^18.1.0",
19 | "react-router-dom": "6",
20 | "react-scripts": "5.0.1",
21 | "reactjs-popup": "^2.0.5",
22 | "tailwind-scrollbar": "^1.3.1",
23 | "typescript": "^4.4.2",
24 | "web-vitals": "^2.1.0"
25 | },
26 | "scripts": {
27 | "start": "react-scripts start",
28 | "build": "react-scripts build",
29 | "test": "react-scripts test",
30 | "eject": "react-scripts eject"
31 | },
32 | "eslintConfig": {
33 | "extends": [
34 | "react-app",
35 | "react-app/jest"
36 | ]
37 | },
38 | "browserslist": {
39 | "production": [
40 | ">0.2%",
41 | "not dead",
42 | "not op_mini all"
43 | ],
44 | "development": [
45 | "last 1 chrome version",
46 | "last 1 firefox version",
47 | "last 1 safari version"
48 | ]
49 | },
50 | "devDependencies": {
51 | "autoprefixer": "^10.4.7",
52 | "postcss": "^8.4.14",
53 | "tailwindcss": "^3.0.24"
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
12 |
13 |
17 |
18 |
27 | React App
28 |
29 |
30 |
31 |
32 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/src/components/Message.tsx:
--------------------------------------------------------------------------------
1 | import { Timestamp } from "firebase/firestore";
2 |
3 | type Props = {
4 | text: string;
5 | sender: boolean;
6 | createdAt: Timestamp;
7 | selectedMessage: string;
8 | messageId: string;
9 | setSelectedMessage: () => void;
10 | isFirstMessage: boolean;
11 | isLastMessage: boolean;
12 | };
13 |
14 | function timeConverter(UNIX_timestamp: Timestamp) {
15 | var a = new Date(UNIX_timestamp.seconds * 1000);
16 | var months = [
17 | "Jan",
18 | "Feb",
19 | "Mar",
20 | "Apr",
21 | "May",
22 | "Jun",
23 | "Jul",
24 | "Aug",
25 | "Sep",
26 | "Oct",
27 | "Nov",
28 | "Dec",
29 | ];
30 | return (
31 | a.getDate() +
32 | " " +
33 | months[a.getMonth()] +
34 | " " +
35 | a.getFullYear() +
36 | " " +
37 | a.getHours() +
38 | ":" +
39 | a.getMinutes()
40 | );
41 | }
42 | export default function Message({
43 | text,
44 | sender,
45 | createdAt,
46 | selectedMessage,
47 | messageId,
48 | setSelectedMessage,
49 | isLastMessage,
50 | isFirstMessage,
51 | }: Props) {
52 | if (sender)
53 | return (
54 |
55 |
65 | {selectedMessage === messageId && (
66 |
{timeConverter(createdAt)}
67 | )}
68 |
69 | );
70 | return (
71 |
72 |
82 | {selectedMessage === messageId && (
83 |
{timeConverter(createdAt)}
84 | )}
85 |
86 | );
87 | }
88 |
--------------------------------------------------------------------------------
/src/pages/Home.tsx:
--------------------------------------------------------------------------------
1 | import { onAuthStateChanged } from "firebase/auth";
2 | import { useEffect, useState } from "react";
3 | import { useNavigate } from "react-router-dom";
4 | import { auth, db } from "../core/firebaseConfig";
5 | import LeftSide from "../components/LeftSide";
6 | import { doc, getDoc } from "firebase/firestore";
7 | import { User } from "../core/types";
8 | import RightSide from "../components/RightSide";
9 |
10 | export default function Home() {
11 | const [selectedChatRoom, setSelectedChatRoom] = useState("");
12 | const [windowWidth, setWindowWidth] = useState(window.innerWidth);
13 | useEffect(() => {
14 | window.addEventListener("resize", () => setWindowWidth(window.innerWidth));
15 | return () =>
16 | window.removeEventListener("resize", () =>
17 | setWindowWidth(window.innerWidth)
18 | );
19 | }, []);
20 | const [user, setUser] = useState();
21 | const [openChat, setOpenChat] = useState(true);
22 | const navigate = useNavigate();
23 | useEffect(() => {
24 | const unsub = onAuthStateChanged(auth, (currentUser) => {
25 | if (currentUser) {
26 | getDoc(doc(db, "users", currentUser.uid)).then((res) => {
27 | setUser({
28 | uid: currentUser.uid,
29 | email: currentUser.email,
30 | birthday: res.data()?.birthday,
31 | fName: res.data()?.fName,
32 | lName: res.data()?.lName,
33 | picture: res.data()?.picture,
34 | });
35 | });
36 | } else {
37 | navigate("/login", { replace: true });
38 | }
39 | });
40 | return unsub;
41 | }, [navigate]);
42 | return (
43 |
44 | {(openChat || windowWidth > 768) && (
45 |
53 | )}
54 | {(!openChat || windowWidth > 768) && (
55 |
63 | )}
64 |
65 | );
66 | }
67 |
--------------------------------------------------------------------------------
/src/components/MessageView.tsx:
--------------------------------------------------------------------------------
1 | import { doc, getDoc } from "firebase/firestore";
2 | import { useEffect, useState } from "react";
3 | import emptyProfile from "../assets/empty-profile.png";
4 | import { db } from "../core/firebaseConfig";
5 |
6 | type Props = {
7 | idUser: string;
8 | lastUpdate: string;
9 | setSelectedChatRoom: (a: string) => void;
10 | selectedChatRoom: string;
11 | chatRoom: string;
12 | lastMessage: string;
13 | searchValue: string;
14 | setOpen: (b: boolean) => void;
15 | };
16 |
17 | type User = {
18 | name: string;
19 | picture: string;
20 | };
21 |
22 | export default function MessageView({
23 | idUser,
24 | lastUpdate,
25 | setSelectedChatRoom,
26 | selectedChatRoom,
27 | chatRoom,
28 | lastMessage,
29 | searchValue,
30 | setOpen,
31 | }: Props) {
32 | const [user, setUser] = useState();
33 | const [loading, setLoading] = useState(false);
34 | useEffect(() => {
35 | setLoading(true);
36 | getDoc(doc(db, "users", idUser))
37 | .then((res) => {
38 | setUser({
39 | name: res.data()?.fName + " " + res.data()?.lName,
40 | picture: res.data()?.picture,
41 | });
42 | })
43 | .finally(() => {
44 | setLoading(false);
45 | });
46 | }, [idUser]);
47 | if (!user?.name.includes(searchValue)) {
48 | return <>>;
49 | }
50 | return (
51 | {
53 | setSelectedChatRoom(chatRoom);
54 | setOpen(false);
55 | }}
56 | className={`h-14 w-full mt-2 flex items-center justify-between ${
57 | selectedChatRoom === chatRoom ? "bg-green bg-opacity-60" : ""
58 | } hover:bg-green hover:bg-opacity-60 rounded-md px-2 py-1 cursor-pointer transition duration-200`}
59 | >
60 |
61 |

66 |
67 |
68 | {loading ? "..." : user?.name}
69 |
70 | {lastMessage && (
71 |
76 | Last Message: {lastMessage.substring(0, 10)}{" "}
77 | {lastMessage.length > 10 && "..."}
78 |
79 | )}
80 |
81 |
82 |
{lastUpdate}
83 |
84 | );
85 | }
86 |
--------------------------------------------------------------------------------
/src/components/MessageForm.tsx:
--------------------------------------------------------------------------------
1 | import EmojiPicker, { IEmojiData } from "emoji-picker-react";
2 | import React, { useEffect, useRef, useState } from "react";
3 |
4 | type Props = {
5 | submitHandler: (e: React.FormEvent) => void;
6 | setNewMessage: (s: string) => void;
7 | loading: boolean;
8 | newMessage: string;
9 | };
10 |
11 | export default function MessageForm({
12 | submitHandler,
13 | setNewMessage,
14 | loading,
15 | newMessage,
16 | }: Props) {
17 | const [openPicker, setOpenPicker] = useState(false);
18 | const pickerRef = useRef(null);
19 | useEffect(() => {
20 | document.addEventListener("click", (e) => {
21 | if (!pickerRef.current?.contains(e.target as Node)) {
22 | setOpenPicker(false);
23 | }
24 | });
25 | return document.removeEventListener("click", (e) => {
26 | if (!pickerRef.current?.contains(e.target as Node)) {
27 | setOpenPicker(false);
28 | }
29 | });
30 | }, []);
31 | const onEmojiClick = (_: React.MouseEvent, emojiObject: IEmojiData) => {
32 | setNewMessage(newMessage + emojiObject.emoji);
33 | };
34 | return (
35 |
81 | );
82 | }
83 |
--------------------------------------------------------------------------------
/src/components/AddUser.tsx:
--------------------------------------------------------------------------------
1 | import EmojiPicker from "emoji-picker-react";
2 | import {
3 | addDoc,
4 | collection,
5 | doc,
6 | DocumentData,
7 | getDocs,
8 | query,
9 | QueryDocumentSnapshot,
10 | QuerySnapshot,
11 | serverTimestamp,
12 | updateDoc,
13 | where,
14 | } from "firebase/firestore";
15 | import { useState } from "react";
16 | import emptyProfile from "../assets/empty-profile.png";
17 | import { db } from "../core/firebaseConfig";
18 |
19 | type TypeSearchResult = {
20 | fName: string;
21 | lName: string;
22 | picture: string;
23 | uid: string;
24 | email: string;
25 | };
26 |
27 | type Props = {
28 | userId: string;
29 | closePopup: () => void;
30 | };
31 |
32 | export default function AddUser({ userId, closePopup }: Props) {
33 | const [search, setSearch] = useState("");
34 | const [searchResult, setSearchResult] = useState();
35 | const [loading, setLoading] = useState(false);
36 | const addHandler = (uid: string) => {
37 | const q = query(
38 | collection(db, "chats"),
39 | where("userIds", "in", [
40 | [userId, uid],
41 | [uid, userId],
42 | ])
43 | );
44 | getDocs(q).then((querySnapshot: QuerySnapshot) => {
45 | if (querySnapshot.empty) {
46 | addDoc(collection(db, "chats"), {
47 | createdAt: serverTimestamp(),
48 | lastMessage: "",
49 | updatedAt: serverTimestamp(),
50 | userIds: [userId, uid],
51 | id: "",
52 | }).then((docRef) => {
53 | updateDoc(doc(db, "chats", docRef.id), { id: docRef.id });
54 | closePopup();
55 | });
56 | } else {
57 | closePopup();
58 | }
59 | });
60 | };
61 | const searchHandler = () => {
62 | setLoading(true);
63 | const q = query(
64 | collection(db, "users"),
65 | where("search", "array-contains", search.toLowerCase())
66 | );
67 | getDocs(q)
68 | .then((querySnapshot: QuerySnapshot) => {
69 | const searchResult: TypeSearchResult[] = [];
70 | querySnapshot.forEach((doc: QueryDocumentSnapshot) => {
71 | searchResult.push({
72 | email: doc.data().email,
73 | fName: doc.data().fName,
74 | lName: doc.data().lName,
75 | picture: doc.data().picture,
76 | uid: doc.data().uid,
77 | });
78 | });
79 | setSearchResult(searchResult);
80 | })
81 | .finally(() => {
82 | setLoading(false);
83 | });
84 | };
85 | return (
86 |
87 |
88 |
89 | Keep in contact with your friends!
90 |
91 |
92 |
122 |
123 | {searchResult?.map((user) => (
124 |
128 |
129 |

134 |
135 |
136 |
137 | {user.fName} {user.lName}
138 |
139 |
{user.email}
140 |
141 |
addHandler(user.uid)}
143 | className="cursor-pointer"
144 | >
145 |
157 |
158 |
159 | ))}
160 |
161 |
162 | );
163 | }
164 |
--------------------------------------------------------------------------------
/src/components/RightSide.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | addDoc,
3 | collection,
4 | doc,
5 | DocumentData,
6 | getDoc,
7 | onSnapshot,
8 | orderBy,
9 | query,
10 | QueryDocumentSnapshot,
11 | QuerySnapshot,
12 | serverTimestamp,
13 | updateDoc,
14 | where,
15 | } from "firebase/firestore";
16 | import React, { useEffect, useRef, useState, memo } from "react";
17 | import emptyProfile from "../assets/empty-profile.png";
18 |
19 | import { db } from "../core/firebaseConfig";
20 | import { Message as MessageType } from "../core/types";
21 | import Message from "./Message";
22 | import Profile from "./Profile";
23 | import ChatHeader from "./ChatHeader";
24 | import MessageForm from "./MessageForm";
25 | type Props = {
26 | chatRoomId: string;
27 | userId?: string;
28 | setOpen: (b: boolean) => void;
29 | lName: string;
30 | fName: string;
31 | picture?: string;
32 | };
33 |
34 | type TypeSelectedFriend = {
35 | picture: string;
36 | lName: string;
37 | fName: string;
38 | email: string;
39 | };
40 |
41 | export default memo(function RightSide({
42 | chatRoomId,
43 | userId,
44 | setOpen,
45 | picture,
46 | fName,
47 | lName,
48 | }: Props) {
49 | const [selectedMessage, setSelectedMessage] = useState("");
50 | const [messages, setMessages] = useState();
51 | const [loading, setLoading] = useState(false);
52 | const [newMessage, setNewMessage] = useState("");
53 | const dummyRef = useRef(null);
54 | const [selectedFriend, setSelectedFriend] = useState();
55 | const submitHandler = (e: React.FormEvent) => {
56 | e.preventDefault();
57 | if (newMessage.trim() !== "") {
58 | setLoading(true);
59 | addDoc(collection(db, "messages"), {
60 | chatId: chatRoomId,
61 | message: newMessage,
62 | sender: userId,
63 | type: "text",
64 | createdAt: serverTimestamp(),
65 | }).then((docRef) => {
66 | updateDoc(doc(db, "messages", docRef.id), {
67 | messageId: docRef.id,
68 | }).then(() => {
69 | updateDoc(doc(db, "chats", chatRoomId), {
70 | updatedAt: serverTimestamp(),
71 | lastMessage: newMessage,
72 | }).finally(() => {
73 | setLoading(false);
74 | setNewMessage("");
75 | });
76 | });
77 | });
78 | dummyRef.current?.scrollIntoView({
79 | behavior: "smooth",
80 | });
81 | }
82 | };
83 | useEffect(() => {
84 | if (chatRoomId) {
85 | const q = query(
86 | collection(db, "messages"),
87 | where("chatId", "==", chatRoomId),
88 | orderBy("createdAt")
89 | );
90 | const unsubscribe = onSnapshot(
91 | q,
92 | (querySnapshot: QuerySnapshot) => {
93 | const messages: MessageType[] = [];
94 | querySnapshot.forEach((doc: QueryDocumentSnapshot) => {
95 | messages.push(doc.data() as MessageType);
96 | });
97 | setMessages(messages);
98 | }
99 | );
100 | return unsubscribe;
101 | }
102 | }, [chatRoomId]);
103 | useEffect(() => {
104 | if (chatRoomId) {
105 | setSelectedFriend({ email: "", fName: "", lName: "", picture: "" });
106 | getDoc(doc(db, "chats", chatRoomId)).then((res) => {
107 | if (res.exists()) {
108 | getDoc(
109 | doc(
110 | db,
111 | "users",
112 | res.data().userIds[0] === userId
113 | ? res.data().userIds[1]
114 | : res.data().userIds[0]
115 | )
116 | ).then((res) => {
117 | if (res.exists()) {
118 | setSelectedFriend(res.data() as TypeSelectedFriend);
119 | }
120 | });
121 | }
122 | });
123 | }
124 | }, [chatRoomId, userId]);
125 | if (!chatRoomId) {
126 | return ;
127 | }
128 | return (
129 |
136 |
143 |
144 | {messages?.map((message, index) => (
145 |
setSelectedMessage(message.messageId)}
153 | isFirstMessage={
154 | index !== 0 && index + 1 !== messages.length
155 | ? message.sender !== messages[index - 1].sender &&
156 | message.sender === messages[index + 1].sender
157 | : index === 0
158 | ? true
159 | : index + 1 === messages.length
160 | ? message.sender !== messages[index - 1].sender
161 | : false
162 | }
163 | isLastMessage={
164 | index !== 0 && index + 1 !== messages.length
165 | ? message.sender !== messages[index + 1].sender &&
166 | message.sender === messages[index - 1].sender
167 | : index + 1 === messages.length
168 | ? true
169 | : index === 0
170 | ? message.sender !== messages[index + 1].sender
171 | : false
172 | }
173 | />
174 | ))}
175 |
176 |
177 |
178 |
184 |
185 |
186 | );
187 | });
188 |
--------------------------------------------------------------------------------
/src/components/LeftSide.tsx:
--------------------------------------------------------------------------------
1 | import { signOut } from "firebase/auth";
2 | import { useEffect, useState, memo, useRef } from "react";
3 | import emptyProfile from "../assets/empty-profile.png";
4 | import { auth, db } from "../core/firebaseConfig";
5 | import {
6 | collection,
7 | DocumentData,
8 | onSnapshot,
9 | orderBy,
10 | query,
11 | QueryDocumentSnapshot,
12 | QuerySnapshot,
13 | Timestamp,
14 | where,
15 | } from "firebase/firestore";
16 | import { chatRoom } from "../core/types";
17 | import MessageView from "./MessageView";
18 | import Popup from "reactjs-popup";
19 | import "reactjs-popup/dist/index.css";
20 | import AddUser from "./AddUser";
21 | import { motion } from "framer-motion";
22 | import { PopupActions } from "reactjs-popup/dist/types";
23 | function timeConverter(UNIX_timestamp: Timestamp): string {
24 | const a = new Date(UNIX_timestamp.seconds * 1000);
25 | return (
26 | a.getHours().toLocaleString("en-US", {
27 | minimumIntegerDigits: 2,
28 | useGrouping: false,
29 | }) +
30 | " : " +
31 | a.getMinutes().toLocaleString("en-US", {
32 | minimumIntegerDigits: 2,
33 | useGrouping: false,
34 | })
35 | );
36 | }
37 | type Props = {
38 | picture?: string;
39 | userId?: string;
40 | displayName: string;
41 | selectedChatRoom: string;
42 | setSelectedChatRoom: (a: string) => void;
43 | setOpen: (b: boolean) => void;
44 | };
45 |
46 | export default memo(function LeftSide({
47 | picture,
48 | userId,
49 | selectedChatRoom,
50 | setSelectedChatRoom,
51 | setOpen,
52 | displayName,
53 | }: Props) {
54 | const [searchValue, setSearchValue] = useState("");
55 | const [chatRooms, setChatRooms] = useState();
56 | const refPopup = useRef(null);
57 | useEffect(() => {
58 | if (userId) {
59 | const q = query(
60 | collection(db, "chats"),
61 | where("userIds", "array-contains", userId),
62 | orderBy("updatedAt")
63 | );
64 | const unsubscribe = onSnapshot(
65 | q,
66 | (querySnapshot: QuerySnapshot) => {
67 | const chatRooms: chatRoom[] = [];
68 | querySnapshot.forEach((doc: QueryDocumentSnapshot) => {
69 | chatRooms.push(doc.data() as chatRoom);
70 | });
71 | setChatRooms(chatRooms.reverse());
72 | }
73 | );
74 | return unsubscribe;
75 | }
76 | }, [userId]);
77 | return (
78 |
84 |
85 |
86 |
{
88 | setSelectedChatRoom("");
89 | setOpen(false);
90 | }}
91 | className="flex items-center space-x-2 cursor-pointer"
92 | >
93 |

98 |
99 |
Messages
100 |
signOut(auth)}
102 | className="bg-green hover:bg-opacity-50 text-white font-medium px-3 py-2 rounded-lg cursor-pointer active:scale-95 transition"
103 | >
104 | Sign out
105 |
106 |
107 |
108 |
109 | You logged in as
110 | {displayName}
111 |
112 |
113 |
114 |
120 |
132 |
133 | Add a new friend
134 |
135 |
136 | }
137 | >
138 |
refPopup.current?.close()}
140 | userId={userId || ""}
141 | />
142 |
143 |
144 |
145 |
159 |
setSearchValue(e.target.value)}
163 | />
164 |
165 |
166 |
167 | {!chatRooms &&
168 | [1, 2, 3, 4].map((item) => (
169 |
182 | ))}
183 | {chatRooms?.map((item: chatRoom) => (
184 |
197 | ))}
198 |
199 |
200 | );
201 | });
202 |
--------------------------------------------------------------------------------
/src/pages/Login.tsx:
--------------------------------------------------------------------------------
1 | import whatsappscreen from "../assets/whatsappscreen.png";
2 | import whatsapplogo from "../assets/whatsapplogo2.png";
3 | import google from "../assets/google.png";
4 | import { useEffect, useState } from "react";
5 | import { Link, useNavigate } from "react-router-dom";
6 | import { auth, db } from "../core/firebaseConfig";
7 | import {
8 | GoogleAuthProvider,
9 | onAuthStateChanged,
10 | signInWithEmailAndPassword,
11 | signInWithPopup,
12 | } from "firebase/auth";
13 | import { LoginType } from "../core/types";
14 | import {
15 | addDoc,
16 | collection,
17 | doc,
18 | getDoc,
19 | serverTimestamp,
20 | setDoc,
21 | updateDoc,
22 | } from "firebase/firestore";
23 |
24 | export default function Login() {
25 | const navigate = useNavigate();
26 | const [data, setData] = useState({ email: "", password: "" });
27 | const [error, setError] = useState("");
28 | const [loading, setLoading] = useState(false);
29 | const onChange = (e: React.ChangeEvent) => {
30 | setData((prevData) => ({ ...prevData, [e.target.name]: e.target.value }));
31 | };
32 | const submitHandler = (e: React.FormEvent) => {
33 | e.preventDefault();
34 | setError("");
35 | setLoading(true);
36 | signInWithEmailAndPassword(auth, data.email, data.password)
37 | .catch((e) => setError(e.code))
38 | .finally(() => setLoading(false));
39 | };
40 | const signInWithGoogleHandler = () => {
41 | const provider = new GoogleAuthProvider();
42 | signInWithPopup(auth, provider).then((result) => {
43 | getDoc(doc(db, "users", result.user.uid))
44 | .then((res) => {
45 | if (!res.exists()) {
46 | const userData = {
47 | fName: result.user.displayName?.split(" ")[0],
48 | lName: result.user.displayName?.split(" ")[1],
49 | birthday: null,
50 | email: result.user.email,
51 | uid: result.user.uid,
52 | picture: result.user.photoURL,
53 | search: [
54 | result.user.displayName?.split(" ")[0].toLowerCase(),
55 | result.user.displayName?.split(" ")[1].toLowerCase(),
56 | result.user.email?.toLowerCase(),
57 | result.user.displayName,
58 | ],
59 | };
60 | setDoc(doc(db, "users", result.user.uid), userData);
61 | addDoc(collection(db, "chats"), {
62 | createdAt: serverTimestamp(),
63 | lastMessage: "",
64 | updatedAt: serverTimestamp(),
65 | userIds: [result.user.uid, "rmbJQucAbjQvll9pV341OB8hxnx2"],
66 | id: "",
67 | }).then((docRef) => {
68 | updateDoc(doc(db, "chats", docRef.id), { id: docRef.id });
69 | addDoc(collection(db, "messages"), {
70 | chatId: docRef.id,
71 | message:
72 | "Hello i'm Islem medjahdi, I'm a computer science student in Algeria - Algiers, If you have any questions, let me know and don't forget to check my portfolio : https://islem-medjahdi-portfolio.vercel.app/",
73 | sender: "rmbJQucAbjQvll9pV341OB8hxnx2",
74 | type: "text",
75 | createdAt: serverTimestamp(),
76 | }).then((docRef) => {
77 | updateDoc(doc(db, "messages", docRef.id), {
78 | messageId: docRef.id,
79 | });
80 | });
81 | updateDoc(doc(db, "chats", docRef.id), {
82 | updatedAt: serverTimestamp(),
83 | lastMessage:
84 | "Hello i'm Islem medjahdi, I'm a computer science student in Algeria - Algiers, If you have any questions, let me know and don't forget to check my portfolio : https://islem-medjahdi-portfolio.vercel.app/",
85 | });
86 | });
87 | }
88 | })
89 | .catch((e) => console.log(e));
90 | });
91 | };
92 | useEffect(() => {
93 | const unsub = onAuthStateChanged(auth, (currentUser) => {
94 | if (currentUser) {
95 | navigate("/", { replace: true });
96 | }
97 | });
98 | return unsub;
99 | }, [navigate]);
100 | return (
101 |
102 |
165 |
166 |
167 | Social media shared today, tomorrow or by location
168 |
169 |
170 |
171 |

176 |
177 |
178 |
179 | );
180 | }
181 |
--------------------------------------------------------------------------------
/src/pages/Signup.tsx:
--------------------------------------------------------------------------------
1 | import google from "../assets/google.png";
2 | import whatsappscreen from "../assets/whatsappscreen.png";
3 | import whatsapplogo from "../assets/whatsapplogo2.png";
4 | import { useEffect, useRef, useState } from "react";
5 | import { Calendar } from "@hassanmojab/react-modern-calendar-datepicker";
6 | import "@hassanmojab/react-modern-calendar-datepicker/lib/DatePicker.css";
7 | import { Link, useNavigate } from "react-router-dom";
8 | import { db, auth } from "../core/firebaseConfig";
9 | import {
10 | createUserWithEmailAndPassword,
11 | onAuthStateChanged,
12 | } from "firebase/auth";
13 | import {
14 | addDoc,
15 | collection,
16 | doc,
17 | serverTimestamp,
18 | setDoc,
19 | updateDoc,
20 | } from "firebase/firestore";
21 | import { BirthdayDate, SignUpType } from "../core/types";
22 |
23 | export default function Signup() {
24 | const navigate = useNavigate();
25 | const [openCalendar, setOpenCalendar] = useState(false);
26 | const calendarRef = useRef(null);
27 | const [error, setError] = useState("");
28 | const [loading, setLoading] = useState(false);
29 | const [data, setData] = useState({
30 | fName: "",
31 | lName: "",
32 | email: "",
33 | password: "",
34 | confirmPassword: "",
35 | birthday: { day: 5, month: 6, year: 2002 },
36 | });
37 | useEffect(() => {
38 | document.addEventListener("click", (e) => {
39 | if (!calendarRef.current?.contains(e.target as Node)) {
40 | setOpenCalendar(false);
41 | }
42 | });
43 | return document.removeEventListener("click", (e) => {
44 | if (!calendarRef.current?.contains(e.target as Node)) {
45 | setOpenCalendar(false);
46 | }
47 | });
48 | }, []);
49 | const onChange = (e: React.ChangeEvent): void => {
50 | setData((prevData) => ({ ...prevData, [e.target.name]: e.target.value }));
51 | };
52 | const dateChangeHandler = (date: BirthdayDate) => {
53 | setData({ ...data, birthday: date });
54 | setOpenCalendar(false);
55 | };
56 | const submitHandler = (e: React.FormEvent) => {
57 | e.preventDefault();
58 | setLoading(true);
59 | setError("");
60 | const { fName, lName, email, password, confirmPassword, birthday } = data;
61 | if (fName.trim() === "") {
62 | setError("First name can not be empty");
63 | setLoading(false);
64 | } else if (lName.trim() === "") {
65 | setLoading(false);
66 | setError("Last name can not be empty");
67 | } else if (password !== confirmPassword) {
68 | setError("Password missmatch");
69 | setLoading(false);
70 | } else {
71 | createUserWithEmailAndPassword(auth, email, password)
72 | .then((user) => {
73 | const userData = {
74 | fName,
75 | lName,
76 | birthday,
77 | email,
78 | uid: user.user.uid,
79 | picture: "",
80 | search: [
81 | lName.toLowerCase(),
82 | fName.toLowerCase(),
83 | email.toLowerCase(),
84 | fName.toLowerCase() + " " + lName.toLowerCase(),
85 | ],
86 | };
87 | setDoc(doc(db, "users", user.user.uid), userData);
88 | addDoc(collection(db, "chats"), {
89 | createdAt: serverTimestamp(),
90 | lastMessage: "",
91 | updatedAt: serverTimestamp(),
92 | userIds: [user.user.uid, "rmbJQucAbjQvll9pV341OB8hxnx2"],
93 | id: "",
94 | }).then((docRef) => {
95 | updateDoc(doc(db, "chats", docRef.id), { id: docRef.id });
96 | addDoc(collection(db, "messages"), {
97 | chatId: docRef.id,
98 | message:
99 | "Hello i'm Islem medjahdi, I'm a computer science student in Algeria - Algiers, If you have any questions, let me know and don't forget to check my portfolio : https://islem-medjahdi-portfolio.vercel.app/",
100 | sender: "rmbJQucAbjQvll9pV341OB8hxnx2",
101 | type: "text",
102 | createdAt: serverTimestamp(),
103 | }).then((docRef) => {
104 | updateDoc(doc(db, "messages", docRef.id), {
105 | messageId: docRef.id,
106 | });
107 | });
108 | updateDoc(doc(db, "chats", docRef.id), {
109 | updatedAt: serverTimestamp(),
110 | message:
111 | "Hello i'm Islem medjahdi, I'm a computer science student in Algeria - Algiers, If you have any questions, let me know and don't forget to check my portfolio : https://islem-medjahdi-portfolio.vercel.app/",
112 | });
113 | });
114 | })
115 | .catch((e) => setError(e.code))
116 | .finally(() => setLoading(false));
117 | }
118 | };
119 | useEffect(() => {
120 | const unsub = onAuthStateChanged(auth, (currentUser) => {
121 | if (currentUser) {
122 | navigate("/", { replace: true });
123 | }
124 | });
125 | return unsub;
126 | }, [navigate]);
127 | return (
128 |
129 |
130 |
131 | Social media shared today, tomorrow or by location
132 |
133 |
134 |
135 |

140 |
141 |
142 |
143 |
144 |

149 |
150 |
261 |
262 |
You already have an account?
263 |
267 | Log in
268 |
269 |
270 |
271 |
272 | );
273 | }
274 |
--------------------------------------------------------------------------------