├── 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 | Screenshot-31 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 | 21 | 26 | 27 |
28 |
29 | whatsapp 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 | 28 | 33 | 34 |
35 |
36 | whatsapp 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 |
63 |

{text}

64 |
65 | {selectedMessage === messageId && ( 66 |

{timeConverter(createdAt)}

67 | )} 68 |
69 | ); 70 | return ( 71 |
72 |
80 |

{text}

81 |
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 | profilePicture 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 |
39 | setNewMessage(e.target.value)} 42 | className="outline-none w-full px-4 py-2 bg-transparent font-medium" 43 | placeholder="Enter a Message" 44 | /> 45 |
46 |
47 | {openPicker && ( 48 |
49 | 50 |
51 | )} 52 | 71 |
72 | 79 |
80 |
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 |
93 |
94 | 102 | 107 | 108 | setSearch(e.target.value)} 110 | className="bg-transparent outline-none w-full" 111 | placeholder="Search" 112 | /> 113 |
114 | 121 |
122 |
123 | {searchResult?.map((user) => ( 124 |
128 |
129 | userpic 134 |
135 |
136 |

137 | {user.fName} {user.lName} 138 |

139 |

{user.email}

140 |
141 |
addHandler(user.uid)} 143 | className="cursor-pointer" 144 | > 145 | 151 | 156 | 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 | profile 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 | 126 | 131 | 132 |

133 | Add a new friend 134 |

135 |
136 | } 137 | > 138 | refPopup.current?.close()} 140 | userId={userId || ""} 141 | /> 142 | 143 |
144 |
145 | 153 | 158 | 159 | setSearchValue(e.target.value)} 163 | /> 164 |
165 |
166 |
167 | {!chatRooms && 168 | [1, 2, 3, 4].map((item) => ( 169 |
173 |
174 |
175 |
176 |
177 |
178 |
179 |
180 |
181 |
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 |
106 |
107 | whatsapplogo 112 |
113 |
114 |

115 | Account login 116 |

117 |
118 |
119 | 120 | 125 |
126 |
127 | 128 | 134 |
135 |
136 | 143 | 151 |
152 | {error && ( 153 |

{error}

154 | )} 155 |
156 |

Don't have an account?

157 | 161 | signup 162 | 163 |
164 |
165 |
166 |

167 | Social media shared today, tomorrow or by location 168 |

169 |
170 |
171 | whatsappscreen 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 | whatsappscreen 140 |
141 |
142 |
143 |
144 | whatsapplogo 149 |
150 |
151 |

152 | Create account 153 |

154 |
155 |
156 | 157 | 162 |
163 |
164 | 165 | 170 |
171 |
172 |
173 |
174 | 175 | 181 |
182 |
183 | 186 |
187 | {`${data.birthday.day} / ${data.birthday.month} / ${data.birthday.year} `} 188 |
189 | setOpenCalendar((prevState) => !prevState)} 191 | xmlns="http://www.w3.org/2000/svg" 192 | className="h-6 w-6 text-gray-300 cursor-pointer" 193 | fill="none" 194 | viewBox="0 0 24 24" 195 | stroke="currentColor" 196 | strokeWidth={2} 197 | > 198 | 203 | 204 | {openCalendar && ( 205 |
206 | 213 |
214 | )} 215 |
216 |
217 |
218 |
219 |
220 |
221 | 222 | 228 |
229 |
230 | 231 | 237 |
238 |
239 |
240 | 247 | 256 |
257 | {error && ( 258 |

{error}

259 | )} 260 |
261 |
262 |

You already have an account?

263 | 267 | Log in 268 | 269 |
270 |
271 |
272 | ); 273 | } 274 | --------------------------------------------------------------------------------