├── .firebase └── hosting.ZGlzdA.cache ├── .firebaserc ├── .gitignore ├── README.md ├── eslint.config.js ├── firebase.json ├── index.html ├── package-lock.json ├── package.json ├── public └── cat.png ├── src ├── App.tsx ├── components │ ├── banner │ │ └── index.tsx │ ├── chat │ │ ├── header.tsx │ │ ├── index.tsx │ │ ├── inputBox.tsx │ │ ├── message.tsx │ │ ├── messageItems │ │ │ ├── index.tsx │ │ │ └── noMessage.tsx │ │ ├── searchBox.tsx │ │ └── usersList.tsx │ ├── login │ │ └── index.tsx │ ├── logout │ │ └── index.tsx │ └── ui │ │ ├── avatar.tsx │ │ ├── button.tsx │ │ ├── checkbox.tsx │ │ ├── close-button.tsx │ │ ├── color-mode.tsx │ │ ├── dialog.tsx │ │ ├── drawer.tsx │ │ ├── field.tsx │ │ ├── input-group.tsx │ │ ├── menu.tsx │ │ ├── popover.tsx │ │ ├── provider.tsx │ │ ├── radio.tsx │ │ ├── slider.tsx │ │ ├── toaster.tsx │ │ └── tooltip.tsx ├── config.ts.example ├── context │ ├── Auth │ │ ├── authReducer.ts │ │ ├── index.tsx │ │ └── types.ts │ └── Chat │ │ ├── chatReducer.ts │ │ ├── index.tsx │ │ └── types.ts ├── index.css ├── main.tsx ├── registerServiceWorker.ts ├── services │ └── index.ts ├── utils │ ├── emoji.ts │ ├── index.ts │ └── useDebounce.ts └── vite-env.d.ts ├── tsconfig.app.json ├── tsconfig.json ├── tsconfig.node.json └── vite.config.ts /.firebase/hosting.ZGlzdA.cache: -------------------------------------------------------------------------------- 1 | index.html,1736764463799,8e88a203a712bc4452ac67fd4d87b297ac005dfee2c87ef425fbc0d5cec952f1 2 | assets/usersList-DuwSJqd5.js,1736764463800,55ef9f48fb8f141fad27d72e533dca5f460ad66a13f6a67fbd57bd7a5fab9dcc 3 | assets/index-Dh_VcYI7.css,1736764463800,0bda23d407d61c1fb857b87e66de12da80178d52eb8f7742f070982d8252000c 4 | assets/input-group-DLWyTOrm.js,1736764463800,5c79902a7d7916e661e3398a59829964d42286f1ceebde7cc36e84073ccec479 5 | cat.png,1736764463602,7795dc3b7296da8d92328b85064ac0ccc02bbb64dbde485df8caf4486f1d327d 6 | assets/avatar-BQIemjMs.js,1736764463799,eeb091ff5f788b0019d5ea03f3d8fb2dd42c8bb0cdc0d6e1001a783cbdfe6fe8 7 | assets/message-DT4wz0Cl.js,1736764463799,7180219909f8074c2b383f55237ba569903e54f7872eb234afd8c09ed274d1a6 8 | assets/header-DNve7lB3.js,1736764463799,9ef8de45221a72ff4bf20aa46e95b4ff6a2fa5aa8bcbad3e2ea20c9ce80a6b33 9 | assets/buttons.esm-DK2fWHEW.js,1736764463800,8144c842086b511c5291346c1bfc399aac1dc47a16e4015dc709016d91b25981 10 | assets/index-BRxWplmp.js,1736764463800,85cc94b9ce11c99e30703f1c876b34a0c66b422c016a2593fcf1bb8cf1dd6472 11 | -------------------------------------------------------------------------------- /.firebaserc: -------------------------------------------------------------------------------- 1 | { 2 | "projects": { 3 | "default": "chatroom-67e21" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | 26 | 27 | # build 28 | /dist 29 | 30 | #config 31 | /src/config.ts 32 | 33 | .firebase 34 | .vscode -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Chat Application using React.JS + Firebase real-time Database + Firebase Gmail Authentication 2 | ===================================== 3 | 4 | I build this app for learning purpose. you can check the live 💁‍♂️ [demo](https://chatroom-67e21.web.app) 5 | 6 | ![image](https://github.com/user-attachments/assets/8d3630ed-b77b-49da-864b-3ef5df6cfbcd) 7 | 8 | 9 | Quick Start: 10 | ------------ 11 | 12 | - ``` git clone ``` 13 | - ``` cd react-firebase-chat ``` 14 | - create a firebase `config.ts` file on /src folder 15 | - ``` npm install ``` 16 | - ``` npm run dev ``` 17 | - ``` npm run build ``` 18 | 19 | 20 | ## Releases notes: 21 | 22 | ### [Version 1.0.0](https://github.com/khyrulAlam/react-firebase-chat/releases/tag/v1.0.0) 23 | 24 | - Initial release of the chat application. 25 | - Implemented real-time messaging using Firebase real-time Database. 26 | - Added Gmail authentication using Firebase Authentication. 27 | - Basic UI for chat interface. 28 | - Deployed live demo. 29 | 30 | 31 | ### [Version 2.0.0](https://github.com/khyrulAlam/react-firebase-chat/releases/tag/v2.0.0) 32 | 33 | - Migrated from React 16 to React 18. 34 | - Added TypeScript for type safety. 35 | - Switched to Vite for faster builds. 36 | - Integrated Chakra UI 3.2.5 with dark theme support. 37 | - Improved overall UI design. 38 | - Refactored codebase from class components to function components. 39 | - Utilized React Context API for state management. 40 | - Various code optimizations and improvements. 41 | 42 | 43 | ### [Version 2.1.0](https://github.com/khyrulAlam/react-firebase-chat/releases/tag/v2.1.0) 44 | 45 | - Contacts Search by name. 46 | - User list sort by created at. 47 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | import js from '@eslint/js' 2 | import globals from 'globals' 3 | import reactHooks from 'eslint-plugin-react-hooks' 4 | import reactRefresh from 'eslint-plugin-react-refresh' 5 | import tseslint from 'typescript-eslint' 6 | 7 | export default tseslint.config( 8 | { ignores: ['dist'] }, 9 | { 10 | extends: [js.configs.recommended, ...tseslint.configs.recommended], 11 | files: ['**/*.{ts,tsx}'], 12 | languageOptions: { 13 | ecmaVersion: 2020, 14 | globals: globals.browser, 15 | }, 16 | plugins: { 17 | 'react-hooks': reactHooks, 18 | 'react-refresh': reactRefresh, 19 | }, 20 | rules: { 21 | ...reactHooks.configs.recommended.rules, 22 | 'react-refresh/only-export-components': [ 23 | 'warn', 24 | { allowConstantExport: true }, 25 | ], 26 | }, 27 | }, 28 | ) 29 | -------------------------------------------------------------------------------- /firebase.json: -------------------------------------------------------------------------------- 1 | { 2 | "hosting": { 3 | "public": "dist", 4 | "ignore": [ 5 | "firebase.json", 6 | "**/.*", 7 | "**/node_modules/**" 8 | ] 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | Cat-Chat { build with react & firebase || This is a learning project } 22 | 23 | 24 | 25 |
26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-firebase-chat", 3 | "private": true, 4 | "version": "2.1.0", 5 | "type": "module", 6 | "author": { 7 | "name": "Khayrul Alam", 8 | "email": "khyrulalam69@gmail.com", 9 | "github": "https://github.com/khyrulAlam", 10 | "linkedin": "https://www.linkedin.com/in/khayrul-alam-360291aa/" 11 | }, 12 | "scripts": { 13 | "dev": "vite", 14 | "build": "tsc -b && vite build", 15 | "lint": "eslint .", 16 | "preview": "vite preview" 17 | }, 18 | "dependencies": { 19 | "@chakra-ui/react": "^3.2.5", 20 | "@emotion/react": "^11.14.0", 21 | "firebase": "^11.1.0", 22 | "next-themes": "^0.4.4", 23 | "react": "^18.3.1", 24 | "react-dom": "^18.3.1", 25 | "react-github-btn": "^1.4.0", 26 | "react-icons": "^5.4.0" 27 | }, 28 | "devDependencies": { 29 | "@eslint/js": "^9.17.0", 30 | "@types/react": "^18.3.18", 31 | "@types/react-dom": "^18.3.5", 32 | "@vitejs/plugin-react": "^4.3.4", 33 | "eslint": "^9.17.0", 34 | "eslint-plugin-react-hooks": "^5.0.0", 35 | "eslint-plugin-react-refresh": "^0.4.16", 36 | "globals": "^15.14.0", 37 | "typescript": "~5.6.2", 38 | "typescript-eslint": "^8.18.2", 39 | "vite": "^6.0.5", 40 | "vite-tsconfig-paths": "^5.1.4" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /public/cat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/khyrulAlam/react-firebase-chat/df1f96bca17002dbddeab2c0c79e08e907a807e8/public/cat.png -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | import { Box, Center, Flex, Spinner } from "@chakra-ui/react"; 2 | import { ColorModeButton } from "@/components/ui/color-mode"; 3 | import { Toaster } from "@/components/ui/toaster"; 4 | import { useAuth } from "./context/Auth"; 5 | import ChatComponent from "./components/chat"; 6 | import Login from "./components/login"; 7 | import Banner from "./components/banner"; 8 | 9 | function App() { 10 | const { isAuthenticated, isLoading } = useAuth(); 11 | 12 | if (isLoading) { 13 | return ( 14 | 20 | 21 | 22 | ); 23 | } 24 | 25 | return ( 26 |
27 | 28 | 29 | 30 | {isAuthenticated ? : } 31 | 32 | 33 |
34 | ); 35 | } 36 | 37 | export default App; 38 | -------------------------------------------------------------------------------- /src/components/banner/index.tsx: -------------------------------------------------------------------------------- 1 | import { Box, Stack } from "@chakra-ui/react"; 2 | import GitHubButton from "react-github-btn"; 3 | 4 | const Banner = () => { 5 | return ( 6 | 7 | 8 | 15 | Star 16 | 17 | 24 | Fork 25 | 26 | 27 | 28 | ); 29 | }; 30 | 31 | export default Banner; 32 | -------------------------------------------------------------------------------- /src/components/chat/header.tsx: -------------------------------------------------------------------------------- 1 | import { Box, Flex, HStack, Image, Text } from "@chakra-ui/react"; 2 | import { Avatar } from "@/components/ui/avatar"; 3 | import { useAuth } from "@/context/Auth"; 4 | import LogoutModal from "../logout"; 5 | 6 | const Header = () => { 7 | const { user } = useAuth(); 8 | 9 | return ( 10 | 17 | 18 | 19 | cat 20 | 21 | Cat Chat 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | {user?.fullName} 31 | 32 | 33 | 34 | 35 | 36 | ); 37 | }; 38 | 39 | export default Header; 40 | -------------------------------------------------------------------------------- /src/components/chat/index.tsx: -------------------------------------------------------------------------------- 1 | import { lazy } from "react"; 2 | import { Flex } from "@chakra-ui/react"; 3 | const Header = lazy(() => import("./header")); 4 | const UsersList = lazy(() => import("./usersList")); 5 | const Message = lazy(() => import("./message")); 6 | import { ChatProvider } from "@/context/Chat"; 7 | 8 | const ChatComponent = () => { 9 | return ( 10 | 11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | ); 20 | }; 21 | 22 | export default ChatComponent; 23 | -------------------------------------------------------------------------------- /src/components/chat/inputBox.tsx: -------------------------------------------------------------------------------- 1 | import { Box, Button, Grid, Input, Text } from "@chakra-ui/react"; 2 | import { useEffect, useState } from "react"; 3 | import { InputGroup } from "../ui/input-group"; 4 | import { emojis } from "@/utils/emoji"; 5 | import { firebaseDatabase } from "@/config"; 6 | import { push, ref } from "firebase/database"; 7 | import { useAuth } from "@/context/Auth"; 8 | import { useChat } from "@/context/Chat"; 9 | 10 | const InputBox = () => { 11 | const { user } = useAuth(); 12 | const { chatRoomId } = useChat(); 13 | const [showEmoji, setShowEmoji] = useState(false); 14 | const [message, setMessage] = useState(""); 15 | 16 | const addNewMessage = (e: React.FormEvent) => { 17 | e.preventDefault(); 18 | if (message.trim()) { 19 | sendMessage(); 20 | } 21 | }; 22 | 23 | const toggleEmoji = () => setShowEmoji((prev) => !prev); 24 | 25 | const handleEmoji = (emoji: string) => setMessage((prev) => prev + emoji); 26 | 27 | const sendMessage = () => { 28 | const collectionRef = ref(firebaseDatabase, chatRoomId); 29 | push(collectionRef, { 30 | uid: user?.uid, 31 | name: user?.userName, 32 | text: message, 33 | time: new Date().toISOString(), 34 | }); 35 | setMessage(""); 36 | }; 37 | 38 | useEffect(() => { 39 | return () => { 40 | setShowEmoji(false); 41 | setMessage(""); 42 | } 43 | }, [chatRoomId]); 44 | 45 | return ( 46 | 47 | {/* @todo: add emoji button */} 48 |
49 | 52 | 😀 53 | 54 | } 55 | width={"full"} 56 | > 57 | setMessage(e.target.value)} 62 | /> 63 | 64 |
65 | 77 | 78 | {emojis.map((emoji, index) => ( 79 | handleEmoji(emoji)} 83 | > 84 | {emoji} 85 | 86 | ))} 87 | 88 | 89 |
90 | ); 91 | }; 92 | 93 | export default InputBox; 94 | -------------------------------------------------------------------------------- /src/components/chat/message.tsx: -------------------------------------------------------------------------------- 1 | import { Box, Flex, HStack, Spinner, Text } from "@chakra-ui/react"; 2 | import { useCallback, useEffect, useState } from "react"; 3 | import InputBox from "./inputBox"; 4 | import { MessageSnapshotResponse } from "@/context/Chat/types"; 5 | import { useChat } from "@/context/Chat"; 6 | import { fetchCommonRoomMessages, fetchOneToOneMessages } from "@/services"; 7 | import MessageItems from "./messageItems"; 8 | import { 9 | limitToLast, 10 | ref, 11 | query, 12 | onChildAdded, 13 | DataSnapshot, 14 | } from "firebase/database"; 15 | import { firebaseDatabase } from "@/config"; 16 | 17 | const Message = () => { 18 | const { chatRoomId, oneToOneRoomId } = useChat(); 19 | const [isLoading, setIsLoading] = useState(true); 20 | const [messages, setMessages] = useState({}); 21 | 22 | // Handle child added 23 | const handleChildAdded = (snapshot: DataSnapshot) => { 24 | if (!snapshot.exists() || !snapshot.key) return; 25 | setMessages((prev) => { 26 | if (prev[snapshot.key as string]) return prev; 27 | return { ...prev, [snapshot.key as string]: snapshot.val() }; 28 | }); 29 | }; 30 | 31 | // Subscribe to room 32 | const subscribeToRoom = useCallback(() => { 33 | const collectionRef = ref(firebaseDatabase, chatRoomId); 34 | const limitedQuery = query(collectionRef, limitToLast(1)); 35 | onChildAdded(limitedQuery, handleChildAdded); 36 | 37 | if (oneToOneRoomId) { 38 | const oneToOneCollectionRef = ref(firebaseDatabase, oneToOneRoomId); 39 | const oneToOneLimitedQuery = query(oneToOneCollectionRef, limitToLast(1)); 40 | onChildAdded(oneToOneLimitedQuery, handleChildAdded); 41 | } 42 | }, [chatRoomId, oneToOneRoomId]); 43 | 44 | // Fetch messages 45 | const fetchData = useCallback(async () => { 46 | setIsLoading(true); 47 | const results = 48 | chatRoomId && oneToOneRoomId 49 | ? await fetchOneToOneMessages(chatRoomId, oneToOneRoomId) 50 | : await fetchCommonRoomMessages(); 51 | setMessages(results); 52 | subscribeToRoom(); 53 | setIsLoading(false); 54 | }, [chatRoomId, oneToOneRoomId, subscribeToRoom]); 55 | 56 | useEffect(() => { 57 | if (!chatRoomId) return; 58 | fetchData(); 59 | }, [chatRoomId, fetchData, oneToOneRoomId]); 60 | 61 | return ( 62 | 69 | 70 | 78 | {/* loading */} 79 | {isLoading && ( 80 | 81 | 82 | Loading messages... 83 | 84 | 85 | 86 | )} 87 | {/* messages */} 88 | {!isLoading && } 89 | 90 | {/* input */} 91 | 92 | 93 | 94 | ); 95 | }; 96 | 97 | export default Message; 98 | -------------------------------------------------------------------------------- /src/components/chat/messageItems/index.tsx: -------------------------------------------------------------------------------- 1 | import { MessageSnapshotResponse } from "@/context/Chat/types"; 2 | import NoMessage from "./noMessage"; 3 | import { Box, HStack, Stack, Text } from "@chakra-ui/react"; 4 | import { Avatar } from "@/components/ui/avatar"; 5 | import { useLayoutEffect, useRef } from "react"; 6 | import { useChat } from "@/context/Chat"; 7 | import { dateFormatter } from "@/utils"; 8 | 9 | interface MessageItemsProps { 10 | messages: MessageSnapshotResponse; 11 | } 12 | 13 | const MessageItems = ({ messages }: MessageItemsProps) => { 14 | const scrollBottomRef = useRef(null); 15 | const { userList } = useChat(); 16 | 17 | useLayoutEffect(() => { 18 | if (scrollBottomRef.current) { 19 | scrollBottomRef.current.scrollIntoView({ behavior: "smooth" }); 20 | } 21 | }, [messages]); 22 | 23 | if (!Object.keys(messages).length) { 24 | return ; 25 | } 26 | return Object.keys(messages)?.map((key, i) => { 27 | const { text, time, uid } = messages[key]; 28 | const isLastMessage = i === Object.keys(messages).length - 1; 29 | return ( 30 | 38 | 39 | 46 | 47 | 48 | 49 | {userList[uid]?.userName ?? "???"} 50 | 51 | 52 | {dateFormatter.format(new Date(time))} 53 | 54 | 55 | {text} 56 | 57 | 58 | 59 | ); 60 | }); 61 | }; 62 | 63 | export default MessageItems; 64 | -------------------------------------------------------------------------------- /src/components/chat/messageItems/noMessage.tsx: -------------------------------------------------------------------------------- 1 | import { Center, Text } from "@chakra-ui/react"; 2 | 3 | const NoMessage = () => { 4 | return
5 | There are no messages yet. 6 |
7 | }; 8 | 9 | export default NoMessage; 10 | -------------------------------------------------------------------------------- /src/components/chat/searchBox.tsx: -------------------------------------------------------------------------------- 1 | import { LuSearch } from "react-icons/lu"; 2 | import { InputGroup } from "../ui/input-group"; 3 | import { Input } from "@chakra-ui/react/input"; 4 | import { useEffect, useState } from "react"; 5 | import useDebounce from "@/utils/useDebounce"; 6 | 7 | interface SearchBoxProps { 8 | setSearch: (value: string) => void; 9 | setLoading: (value: boolean) => void; 10 | } 11 | 12 | const SearchBox = ({ setSearch, setLoading }: SearchBoxProps) => { 13 | const [searchToken, setSearchToken] = useState(null); 14 | const debouncedSearchToken = useDebounce(searchToken, 500); 15 | 16 | useEffect(() => { 17 | setSearch(debouncedSearchToken || ""); 18 | }, [debouncedSearchToken]); 19 | 20 | return ( 21 | }> 22 | { 26 | setLoading(true); 27 | setSearchToken(e.currentTarget.value) 28 | } 29 | } 30 | /> 31 | 32 | ); 33 | }; 34 | 35 | export default SearchBox; 36 | -------------------------------------------------------------------------------- /src/components/chat/usersList.tsx: -------------------------------------------------------------------------------- 1 | import { Box, HStack, Spinner, Stack, Text } from "@chakra-ui/react"; 2 | import { Avatar, AvatarGroup } from "@/components/ui/avatar"; 3 | import { useEffect, useState } from "react"; 4 | import { User } from "@/context/Auth/types"; 5 | import { useAuth } from "@/context/Auth"; 6 | import { useChat, useChatDispatch } from "@/context/Chat"; 7 | import { ChatActionType } from "@/context/Chat/types"; 8 | import { unSubscribeDatabase } from "@/services"; 9 | import { firebaseDatabase } from "@/config"; 10 | import { onValue, ref } from "firebase/database"; 11 | import { DB_NAME, numberFormatter } from "@/utils"; 12 | import SearchBox from "./searchBox"; 13 | 14 | const UsersList = () => { 15 | const [loading, setLoading] = useState(true); 16 | const { user: authUser } = useAuth(); 17 | const { chatRoomId, oneToOneRoomId } = useChat(); 18 | const dispatch = useChatDispatch(); 19 | const { userList: storeUserList } = useChat(); 20 | const [userList, setUserList] = useState([]); 21 | const [userCount, setUserCount] = useState(0); 22 | const [startSearch, setStartSearch] = useState(false); 23 | 24 | useEffect(() => { 25 | setLoading(true); 26 | const collectionRef = ref(firebaseDatabase, DB_NAME.USER_TABLE); 27 | const unsubscribe = onValue(collectionRef, (snapshot) => { 28 | const data = snapshot.val(); 29 | if (data) { 30 | const users = Object.values(data) as User[]; 31 | setUserCount(users.length || 0); 32 | dispatch({ 33 | type: ChatActionType.SET_USER_LIST, 34 | payload: data, 35 | }); 36 | } 37 | setLoading(false); 38 | }); 39 | 40 | return () => { 41 | unsubscribe(); 42 | }; 43 | }, [authUser?.uid, dispatch]); 44 | 45 | const handleUserClick = (user: User | string) => { 46 | const newChatRoomId = 47 | typeof user === "string" 48 | ? DB_NAME.CHAT_ROOM 49 | : `${authUser?.uid}+${user.uid}`; 50 | const newOneToOneRoomId = 51 | typeof user !== "string" ? `${user.uid}+${authUser?.uid}` : ""; 52 | 53 | // Unsubscribe from the database 54 | const roomList = [chatRoomId, oneToOneRoomId].filter(Boolean); 55 | unSubscribeDatabase(roomList); 56 | 57 | // Set the new chat room id 58 | dispatch({ 59 | type: ChatActionType.SET_CHAT_ROOM_ID, 60 | payload: { 61 | chatRoomId: newChatRoomId, 62 | oneToOneRoomId: newOneToOneRoomId, 63 | }, 64 | }); 65 | }; 66 | 67 | const handleSearchUserList = (token: string) => { 68 | const sourceList = Object.values(storeUserList) as User[]; 69 | const regex = new RegExp(token, "i"); 70 | const result = sourceList 71 | .filter((user) => user.userName.toLowerCase().match(regex)) 72 | .sort((a, b) => (a.createdAt < b.createdAt ? 1 : -1)); 73 | setUserList(result); 74 | setStartSearch(false); 75 | }; 76 | 77 | return ( 78 | 87 | {loading && } 88 | {!loading && ( 89 | 90 | 94 | handleUserClick(DB_NAME.CHAT_ROOM)} 103 | > 104 | 105 | 110 | 114 | 115 | 116 | Common Group 117 | 118 | 119 | 120 | {startSearch ? ( 121 | 122 | 123 | 124 | ) : userList.length < 1 ? ( 125 | 126 | 127 | No Search result found 128 | 129 | 130 | ) : ( 131 | userList?.map((user) => ( 132 | handleUserClick(user)} 150 | > 151 | 157 | 158 | 159 | {user.userName} 160 | 161 | 162 | 163 | )) 164 | )} 165 | 166 | )} 167 | 168 | ); 169 | }; 170 | 171 | export default UsersList; 172 | -------------------------------------------------------------------------------- /src/components/login/index.tsx: -------------------------------------------------------------------------------- 1 | import { Flex, Button, Image, Text, Card } from "@chakra-ui/react"; 2 | import { useAuthDispatch } from "@/context/Auth"; 3 | import { AuthActionEnum, User } from "@/context/Auth/types"; 4 | import { 5 | GoogleAuthProvider, 6 | signInWithPopup, 7 | getAdditionalUserInfo, 8 | } from "firebase/auth"; 9 | import { firebaseAuth } from "@/config"; 10 | import { addUserInfo } from "@/services"; 11 | import { toaster } from "@/components/ui/toaster"; 12 | 13 | const Login = () => { 14 | const dispatch = useAuthDispatch(); 15 | 16 | const handleLogin = async () => { 17 | try { 18 | const result = await signInWithPopup( 19 | firebaseAuth, 20 | new GoogleAuthProvider() 21 | ); 22 | const additionalUserInfo = getAdditionalUserInfo(result); 23 | const userInfo: User = { 24 | email: result.user.email || "", 25 | fullName: result.user.displayName || "", 26 | profile_picture: result.user.photoURL || "", 27 | uid: result.user.uid, 28 | userName: (additionalUserInfo?.profile?.given_name as string) || "", 29 | createdAt: new Date().getTime(), 30 | }; 31 | 32 | await addUserInfo(userInfo); 33 | 34 | dispatch({ 35 | type: AuthActionEnum.SET_USER, 36 | payload: userInfo, 37 | }); 38 | 39 | toaster.create({ 40 | title: "Success", 41 | description: "Login successful", 42 | type: "success", 43 | }); 44 | } catch (error) { 45 | console.error("error", error); 46 | toaster.create({ 47 | title: "Error", 48 | description: (error as Error)?.message, 49 | type: "error", 50 | }); 51 | } 52 | }; 53 | 54 | return ( 55 | 62 | 70 | logo 71 | 77 | Cat Chat 78 | 79 | 92 | 93 | 94 | ); 95 | }; 96 | 97 | export default Login; 98 | -------------------------------------------------------------------------------- /src/components/logout/index.tsx: -------------------------------------------------------------------------------- 1 | import { LuLogOut } from "react-icons/lu"; 2 | import { 3 | DialogActionTrigger, 4 | DialogBody, 5 | DialogCloseTrigger, 6 | DialogContent, 7 | DialogFooter, 8 | DialogHeader, 9 | DialogRoot, 10 | DialogTitle, 11 | DialogTrigger, 12 | } from "../ui/dialog"; 13 | import { Button } from "@chakra-ui/react"; 14 | import { useAuthDispatch } from "@/context/Auth"; 15 | import { signOut } from "firebase/auth"; 16 | import { firebaseAuth } from "@/config"; 17 | import { AuthActionEnum } from "@/context/Auth/types"; 18 | import { toaster } from "../ui/toaster"; 19 | 20 | const LogoutModal = () => { 21 | const dispatch = useAuthDispatch(); 22 | const logout = async () => { 23 | try { 24 | await signOut(firebaseAuth); 25 | dispatch({ type: AuthActionEnum.LOGOUT }); 26 | } catch (error) { 27 | toaster.create({ 28 | title: "Error", 29 | description: (error as Error)?.message, 30 | type: "error", 31 | }); 32 | } 33 | }; 34 | 35 | return ( 36 | 37 | 38 | 41 | 42 | 43 | 44 | Confirm Logout 45 | 46 | 47 |

Are you sure you want to logout?

48 |
49 | 50 | 51 | 52 | 53 | 56 | 57 | 58 |
59 |
60 | ); 61 | }; 62 | 63 | export default LogoutModal; 64 | -------------------------------------------------------------------------------- /src/components/ui/avatar.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import type { GroupProps, SlotRecipeProps } from "@chakra-ui/react" 4 | import { Avatar as ChakraAvatar, Group } from "@chakra-ui/react" 5 | import * as React from "react" 6 | 7 | type ImageProps = React.ImgHTMLAttributes 8 | 9 | export interface AvatarProps extends ChakraAvatar.RootProps { 10 | name?: string 11 | src?: string 12 | srcSet?: string 13 | loading?: ImageProps["loading"] 14 | icon?: React.ReactElement 15 | fallback?: React.ReactNode 16 | } 17 | 18 | export const Avatar = React.forwardRef( 19 | function Avatar(props, ref) { 20 | const { name, src, srcSet, loading, icon, fallback, children, ...rest } = 21 | props 22 | return ( 23 | 24 | 25 | {fallback} 26 | 27 | 28 | {children} 29 | 30 | ) 31 | }, 32 | ) 33 | 34 | interface AvatarFallbackProps extends ChakraAvatar.FallbackProps { 35 | name?: string 36 | icon?: React.ReactElement 37 | } 38 | 39 | const AvatarFallback = React.forwardRef( 40 | function AvatarFallback(props, ref) { 41 | const { name, icon, children, ...rest } = props 42 | return ( 43 | 44 | {children} 45 | {name != null && children == null && <>{getInitials(name)}} 46 | {name == null && children == null && ( 47 | {icon} 48 | )} 49 | 50 | ) 51 | }, 52 | ) 53 | 54 | function getInitials(name: string) { 55 | const names = name.trim().split(" ") 56 | const firstName = names[0] != null ? names[0] : "" 57 | const lastName = names.length > 1 ? names[names.length - 1] : "" 58 | return firstName && lastName 59 | ? `${firstName.charAt(0)}${lastName.charAt(0)}` 60 | : firstName.charAt(0) 61 | } 62 | 63 | interface AvatarGroupProps extends GroupProps, SlotRecipeProps<"avatar"> {} 64 | 65 | export const AvatarGroup = React.forwardRef( 66 | function AvatarGroup(props, ref) { 67 | const { size, variant, borderless, ...rest } = props 68 | return ( 69 | 70 | 71 | 72 | ) 73 | }, 74 | ) 75 | -------------------------------------------------------------------------------- /src/components/ui/button.tsx: -------------------------------------------------------------------------------- 1 | import type { ButtonProps as ChakraButtonProps } from "@chakra-ui/react" 2 | import { 3 | AbsoluteCenter, 4 | Button as ChakraButton, 5 | Span, 6 | Spinner, 7 | } from "@chakra-ui/react" 8 | import * as React from "react" 9 | 10 | interface ButtonLoadingProps { 11 | loading?: boolean 12 | loadingText?: React.ReactNode 13 | } 14 | 15 | export interface ButtonProps extends ChakraButtonProps, ButtonLoadingProps {} 16 | 17 | export const Button = React.forwardRef( 18 | function Button(props, ref) { 19 | const { loading, disabled, loadingText, children, ...rest } = props 20 | return ( 21 | 22 | {loading && !loadingText ? ( 23 | <> 24 | 25 | 26 | 27 | {children} 28 | 29 | ) : loading && loadingText ? ( 30 | <> 31 | 32 | {loadingText} 33 | 34 | ) : ( 35 | children 36 | )} 37 | 38 | ) 39 | }, 40 | ) 41 | -------------------------------------------------------------------------------- /src/components/ui/checkbox.tsx: -------------------------------------------------------------------------------- 1 | import { Checkbox as ChakraCheckbox } from "@chakra-ui/react" 2 | import * as React from "react" 3 | 4 | export interface CheckboxProps extends ChakraCheckbox.RootProps { 5 | icon?: React.ReactNode 6 | inputProps?: React.InputHTMLAttributes 7 | rootRef?: React.Ref 8 | } 9 | 10 | export const Checkbox = React.forwardRef( 11 | function Checkbox(props, ref) { 12 | const { icon, children, inputProps, rootRef, ...rest } = props 13 | return ( 14 | 15 | 16 | 17 | {icon || } 18 | 19 | {children != null && ( 20 | {children} 21 | )} 22 | 23 | ) 24 | }, 25 | ) 26 | -------------------------------------------------------------------------------- /src/components/ui/close-button.tsx: -------------------------------------------------------------------------------- 1 | import type { ButtonProps } from "@chakra-ui/react" 2 | import { IconButton as ChakraIconButton } from "@chakra-ui/react" 3 | import * as React from "react" 4 | import { LuX } from "react-icons/lu" 5 | 6 | export type CloseButtonProps = ButtonProps 7 | 8 | export const CloseButton = React.forwardRef< 9 | HTMLButtonElement, 10 | CloseButtonProps 11 | >(function CloseButton(props, ref) { 12 | return ( 13 | 14 | {props.children ?? } 15 | 16 | ) 17 | }) 18 | -------------------------------------------------------------------------------- /src/components/ui/color-mode.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable react-refresh/only-export-components */ 2 | /* eslint-disable @typescript-eslint/no-empty-object-type */ 3 | "use client" 4 | 5 | import type { IconButtonProps } from "@chakra-ui/react" 6 | import { ClientOnly, IconButton, Skeleton } from "@chakra-ui/react" 7 | import { ThemeProvider, useTheme } from "next-themes" 8 | import type { ThemeProviderProps } from "next-themes" 9 | import * as React from "react" 10 | import { LuMoon, LuSun } from "react-icons/lu" 11 | 12 | export interface ColorModeProviderProps extends ThemeProviderProps {} 13 | 14 | export function ColorModeProvider(props: ColorModeProviderProps) { 15 | return ( 16 | 17 | ) 18 | } 19 | 20 | export function useColorMode() { 21 | const { resolvedTheme, setTheme } = useTheme() 22 | const toggleColorMode = () => { 23 | setTheme(resolvedTheme === "light" ? "dark" : "light") 24 | } 25 | return { 26 | colorMode: resolvedTheme, 27 | setColorMode: setTheme, 28 | toggleColorMode, 29 | } 30 | } 31 | 32 | export function useColorModeValue(light: T, dark: T) { 33 | const { colorMode } = useColorMode() 34 | return colorMode === "light" ? light : dark 35 | } 36 | 37 | export function ColorModeIcon() { 38 | const { colorMode } = useColorMode() 39 | return colorMode === "light" ? : 40 | } 41 | 42 | interface ColorModeButtonProps extends Omit {} 43 | 44 | export const ColorModeButton = React.forwardRef< 45 | HTMLButtonElement, 46 | ColorModeButtonProps 47 | >(function ColorModeButton(props, ref) { 48 | const { toggleColorMode } = useColorMode() 49 | return ( 50 | }> 51 | 65 | 66 | 67 | 68 | ) 69 | }) 70 | -------------------------------------------------------------------------------- /src/components/ui/dialog.tsx: -------------------------------------------------------------------------------- 1 | import { Dialog as ChakraDialog, Portal } from "@chakra-ui/react" 2 | import { CloseButton } from "./close-button" 3 | import * as React from "react" 4 | 5 | interface DialogContentProps extends ChakraDialog.ContentProps { 6 | portalled?: boolean 7 | portalRef?: React.RefObject 8 | backdrop?: boolean 9 | } 10 | 11 | export const DialogContent = React.forwardRef< 12 | HTMLDivElement, 13 | DialogContentProps 14 | >(function DialogContent(props, ref) { 15 | const { 16 | children, 17 | portalled = true, 18 | portalRef, 19 | backdrop = true, 20 | ...rest 21 | } = props 22 | 23 | return ( 24 | 25 | {backdrop && } 26 | 27 | 28 | {children} 29 | 30 | 31 | 32 | ) 33 | }) 34 | 35 | export const DialogCloseTrigger = React.forwardRef< 36 | HTMLButtonElement, 37 | ChakraDialog.CloseTriggerProps 38 | >(function DialogCloseTrigger(props, ref) { 39 | return ( 40 | 47 | 48 | {props.children} 49 | 50 | 51 | ) 52 | }) 53 | 54 | export const DialogRoot = ChakraDialog.Root 55 | export const DialogFooter = ChakraDialog.Footer 56 | export const DialogHeader = ChakraDialog.Header 57 | export const DialogBody = ChakraDialog.Body 58 | export const DialogBackdrop = ChakraDialog.Backdrop 59 | export const DialogTitle = ChakraDialog.Title 60 | export const DialogDescription = ChakraDialog.Description 61 | export const DialogTrigger = ChakraDialog.Trigger 62 | export const DialogActionTrigger = ChakraDialog.ActionTrigger 63 | -------------------------------------------------------------------------------- /src/components/ui/drawer.tsx: -------------------------------------------------------------------------------- 1 | import { Drawer as ChakraDrawer, Portal } from "@chakra-ui/react" 2 | import { CloseButton } from "./close-button" 3 | import * as React from "react" 4 | 5 | interface DrawerContentProps extends ChakraDrawer.ContentProps { 6 | portalled?: boolean 7 | portalRef?: React.RefObject 8 | offset?: ChakraDrawer.ContentProps["padding"] 9 | } 10 | 11 | export const DrawerContent = React.forwardRef< 12 | HTMLDivElement, 13 | DrawerContentProps 14 | >(function DrawerContent(props, ref) { 15 | const { children, portalled = true, portalRef, offset, ...rest } = props 16 | return ( 17 | 18 | 19 | 20 | {children} 21 | 22 | 23 | 24 | ) 25 | }) 26 | 27 | export const DrawerCloseTrigger = React.forwardRef< 28 | HTMLButtonElement, 29 | ChakraDrawer.CloseTriggerProps 30 | >(function DrawerCloseTrigger(props, ref) { 31 | return ( 32 | 39 | 40 | 41 | ) 42 | }) 43 | 44 | export const DrawerTrigger = ChakraDrawer.Trigger 45 | export const DrawerRoot = ChakraDrawer.Root 46 | export const DrawerFooter = ChakraDrawer.Footer 47 | export const DrawerHeader = ChakraDrawer.Header 48 | export const DrawerBody = ChakraDrawer.Body 49 | export const DrawerBackdrop = ChakraDrawer.Backdrop 50 | export const DrawerDescription = ChakraDrawer.Description 51 | export const DrawerTitle = ChakraDrawer.Title 52 | export const DrawerActionTrigger = ChakraDrawer.ActionTrigger 53 | -------------------------------------------------------------------------------- /src/components/ui/field.tsx: -------------------------------------------------------------------------------- 1 | import { Field as ChakraField } from "@chakra-ui/react" 2 | import * as React from "react" 3 | 4 | export interface FieldProps extends Omit { 5 | label?: React.ReactNode 6 | helperText?: React.ReactNode 7 | errorText?: React.ReactNode 8 | optionalText?: React.ReactNode 9 | } 10 | 11 | export const Field = React.forwardRef( 12 | function Field(props, ref) { 13 | const { label, children, helperText, errorText, optionalText, ...rest } = 14 | props 15 | return ( 16 | 17 | {label && ( 18 | 19 | {label} 20 | 21 | 22 | )} 23 | {children} 24 | {helperText && ( 25 | {helperText} 26 | )} 27 | {errorText && ( 28 | {errorText} 29 | )} 30 | 31 | ) 32 | }, 33 | ) 34 | -------------------------------------------------------------------------------- /src/components/ui/input-group.tsx: -------------------------------------------------------------------------------- 1 | import type { BoxProps, InputElementProps } from "@chakra-ui/react" 2 | import { Group, InputElement } from "@chakra-ui/react" 3 | import * as React from "react" 4 | 5 | export interface InputGroupProps extends BoxProps { 6 | startElementProps?: InputElementProps 7 | endElementProps?: InputElementProps 8 | startElement?: React.ReactNode 9 | endElement?: React.ReactNode 10 | children: React.ReactElement 11 | startOffset?: InputElementProps["paddingStart"] 12 | endOffset?: InputElementProps["paddingEnd"] 13 | } 14 | 15 | export const InputGroup = React.forwardRef( 16 | function InputGroup(props, ref) { 17 | const { 18 | startElement, 19 | startElementProps, 20 | endElement, 21 | endElementProps, 22 | children, 23 | startOffset = "6px", 24 | endOffset = "6px", 25 | ...rest 26 | } = props 27 | 28 | const child = 29 | React.Children.only>(children) 30 | 31 | return ( 32 | 33 | {startElement && ( 34 | 35 | {startElement} 36 | 37 | )} 38 | {React.cloneElement(child, { 39 | ...(startElement && { 40 | ps: `calc(var(--input-height) - ${startOffset})`, 41 | }), 42 | ...(endElement && { pe: `calc(var(--input-height) - ${endOffset})` }), 43 | ...children.props, 44 | })} 45 | {endElement && ( 46 | 47 | {endElement} 48 | 49 | )} 50 | 51 | ) 52 | }, 53 | ) 54 | -------------------------------------------------------------------------------- /src/components/ui/menu.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { AbsoluteCenter, Menu as ChakraMenu, Portal } from "@chakra-ui/react" 4 | import * as React from "react" 5 | import { LuCheck, LuChevronRight } from "react-icons/lu" 6 | 7 | interface MenuContentProps extends ChakraMenu.ContentProps { 8 | portalled?: boolean 9 | portalRef?: React.RefObject 10 | } 11 | 12 | export const MenuContent = React.forwardRef( 13 | function MenuContent(props, ref) { 14 | const { portalled = true, portalRef, ...rest } = props 15 | return ( 16 | 17 | 18 | 19 | 20 | 21 | ) 22 | }, 23 | ) 24 | 25 | export const MenuArrow = React.forwardRef< 26 | HTMLDivElement, 27 | ChakraMenu.ArrowProps 28 | >(function MenuArrow(props, ref) { 29 | return ( 30 | 31 | 32 | 33 | ) 34 | }) 35 | 36 | export const MenuCheckboxItem = React.forwardRef< 37 | HTMLDivElement, 38 | ChakraMenu.CheckboxItemProps 39 | >(function MenuCheckboxItem(props, ref) { 40 | return ( 41 | 42 | 45 | {props.children} 46 | 47 | ) 48 | }) 49 | 50 | export const MenuRadioItem = React.forwardRef< 51 | HTMLDivElement, 52 | ChakraMenu.RadioItemProps 53 | >(function MenuRadioItem(props, ref) { 54 | const { children, ...rest } = props 55 | return ( 56 | 57 | 58 | 59 | 60 | 61 | 62 | {children} 63 | 64 | ) 65 | }) 66 | 67 | export const MenuItemGroup = React.forwardRef< 68 | HTMLDivElement, 69 | ChakraMenu.ItemGroupProps 70 | >(function MenuItemGroup(props, ref) { 71 | const { title, children, ...rest } = props 72 | return ( 73 | 74 | {title && ( 75 | 76 | {title} 77 | 78 | )} 79 | {children} 80 | 81 | ) 82 | }) 83 | 84 | export interface MenuTriggerItemProps extends ChakraMenu.ItemProps { 85 | startIcon?: React.ReactNode 86 | } 87 | 88 | export const MenuTriggerItem = React.forwardRef< 89 | HTMLDivElement, 90 | MenuTriggerItemProps 91 | >(function MenuTriggerItem(props, ref) { 92 | const { startIcon, children, ...rest } = props 93 | return ( 94 | 95 | {startIcon} 96 | {children} 97 | 98 | 99 | ) 100 | }) 101 | 102 | export const MenuRadioItemGroup = ChakraMenu.RadioItemGroup 103 | export const MenuContextTrigger = ChakraMenu.ContextTrigger 104 | export const MenuRoot = ChakraMenu.Root 105 | export const MenuSeparator = ChakraMenu.Separator 106 | 107 | export const MenuItem = ChakraMenu.Item 108 | export const MenuItemText = ChakraMenu.ItemText 109 | export const MenuItemCommand = ChakraMenu.ItemCommand 110 | export const MenuTrigger = ChakraMenu.Trigger 111 | -------------------------------------------------------------------------------- /src/components/ui/popover.tsx: -------------------------------------------------------------------------------- 1 | import { Popover as ChakraPopover, Portal } from "@chakra-ui/react" 2 | import { CloseButton } from "./close-button" 3 | import * as React from "react" 4 | 5 | interface PopoverContentProps extends ChakraPopover.ContentProps { 6 | portalled?: boolean 7 | portalRef?: React.RefObject 8 | } 9 | 10 | export const PopoverContent = React.forwardRef< 11 | HTMLDivElement, 12 | PopoverContentProps 13 | >(function PopoverContent(props, ref) { 14 | const { portalled = true, portalRef, ...rest } = props 15 | return ( 16 | 17 | 18 | 19 | 20 | 21 | ) 22 | }) 23 | 24 | export const PopoverArrow = React.forwardRef< 25 | HTMLDivElement, 26 | ChakraPopover.ArrowProps 27 | >(function PopoverArrow(props, ref) { 28 | return ( 29 | 30 | 31 | 32 | ) 33 | }) 34 | 35 | export const PopoverCloseTrigger = React.forwardRef< 36 | HTMLButtonElement, 37 | ChakraPopover.CloseTriggerProps 38 | >(function PopoverCloseTrigger(props, ref) { 39 | return ( 40 | 48 | 49 | 50 | ) 51 | }) 52 | 53 | export const PopoverTitle = ChakraPopover.Title 54 | export const PopoverDescription = ChakraPopover.Description 55 | export const PopoverFooter = ChakraPopover.Footer 56 | export const PopoverHeader = ChakraPopover.Header 57 | export const PopoverRoot = ChakraPopover.Root 58 | export const PopoverBody = ChakraPopover.Body 59 | export const PopoverTrigger = ChakraPopover.Trigger 60 | -------------------------------------------------------------------------------- /src/components/ui/provider.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { ChakraProvider, defaultSystem } from "@chakra-ui/react" 4 | import { 5 | ColorModeProvider, 6 | type ColorModeProviderProps, 7 | } from "./color-mode" 8 | 9 | export function Provider(props: ColorModeProviderProps) { 10 | return ( 11 | 12 | 13 | 14 | ) 15 | } 16 | -------------------------------------------------------------------------------- /src/components/ui/radio.tsx: -------------------------------------------------------------------------------- 1 | import { RadioGroup as ChakraRadioGroup } from "@chakra-ui/react" 2 | import * as React from "react" 3 | 4 | export interface RadioProps extends ChakraRadioGroup.ItemProps { 5 | rootRef?: React.Ref 6 | inputProps?: React.InputHTMLAttributes 7 | } 8 | 9 | export const Radio = React.forwardRef( 10 | function Radio(props, ref) { 11 | const { children, inputProps, rootRef, ...rest } = props 12 | return ( 13 | 14 | 15 | 16 | {children && ( 17 | {children} 18 | )} 19 | 20 | ) 21 | }, 22 | ) 23 | 24 | export const RadioGroup = ChakraRadioGroup.Root 25 | -------------------------------------------------------------------------------- /src/components/ui/slider.tsx: -------------------------------------------------------------------------------- 1 | import { Slider as ChakraSlider, For, HStack } from "@chakra-ui/react" 2 | import * as React from "react" 3 | 4 | export interface SliderProps extends ChakraSlider.RootProps { 5 | marks?: Array 6 | label?: React.ReactNode 7 | showValue?: boolean 8 | } 9 | 10 | export const Slider = React.forwardRef( 11 | function Slider(props, ref) { 12 | const { marks: marksProp, label, showValue, ...rest } = props 13 | const value = props.defaultValue ?? props.value 14 | 15 | const marks = marksProp?.map((mark) => { 16 | if (typeof mark === "number") return { value: mark, label: undefined } 17 | return mark 18 | }) 19 | 20 | const hasMarkLabel = !!marks?.some((mark) => mark.label) 21 | 22 | return ( 23 | 24 | {label && !showValue && ( 25 | {label} 26 | )} 27 | {label && showValue && ( 28 | 29 | {label} 30 | 31 | 32 | )} 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | ) 42 | }, 43 | ) 44 | 45 | function SliderThumbs(props: { value?: number[] }) { 46 | const { value } = props 47 | return ( 48 | 49 | {(_, index) => ( 50 | 51 | 52 | 53 | )} 54 | 55 | ) 56 | } 57 | 58 | interface SliderMarksProps { 59 | marks?: Array 60 | } 61 | 62 | const SliderMarks = React.forwardRef( 63 | function SliderMarks(props, ref) { 64 | const { marks } = props 65 | if (!marks?.length) return null 66 | 67 | return ( 68 | 69 | {marks.map((mark, index) => { 70 | const value = typeof mark === "number" ? mark : mark.value 71 | const label = typeof mark === "number" ? undefined : mark.label 72 | return ( 73 | 74 | 75 | {label} 76 | 77 | ) 78 | })} 79 | 80 | ) 81 | }, 82 | ) 83 | -------------------------------------------------------------------------------- /src/components/ui/toaster.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable react-refresh/only-export-components */ 2 | "use client" 3 | 4 | import { 5 | Toaster as ChakraToaster, 6 | Portal, 7 | Spinner, 8 | Stack, 9 | Toast, 10 | createToaster, 11 | } from "@chakra-ui/react" 12 | 13 | export const toaster = createToaster({ 14 | placement: "bottom-end", 15 | pauseOnPageIdle: true, 16 | }) 17 | 18 | export const Toaster = () => { 19 | return ( 20 | 21 | 22 | {(toast) => ( 23 | 24 | {toast.type === "loading" ? ( 25 | 26 | ) : ( 27 | 28 | )} 29 | 30 | {toast.title && {toast.title}} 31 | {toast.description && ( 32 | {toast.description} 33 | )} 34 | 35 | {toast.action && ( 36 | {toast.action.label} 37 | )} 38 | {toast.meta?.closable && } 39 | 40 | )} 41 | 42 | 43 | ) 44 | } 45 | -------------------------------------------------------------------------------- /src/components/ui/tooltip.tsx: -------------------------------------------------------------------------------- 1 | import { Tooltip as ChakraTooltip, Portal } from "@chakra-ui/react" 2 | import * as React from "react" 3 | 4 | export interface TooltipProps extends ChakraTooltip.RootProps { 5 | showArrow?: boolean 6 | portalled?: boolean 7 | portalRef?: React.RefObject 8 | content: React.ReactNode 9 | contentProps?: ChakraTooltip.ContentProps 10 | disabled?: boolean 11 | } 12 | 13 | export const Tooltip = React.forwardRef( 14 | function Tooltip(props, ref) { 15 | const { 16 | showArrow, 17 | children, 18 | disabled, 19 | portalled, 20 | content, 21 | contentProps, 22 | portalRef, 23 | ...rest 24 | } = props 25 | 26 | if (disabled) return children 27 | 28 | return ( 29 | 30 | {children} 31 | 32 | 33 | 34 | {showArrow && ( 35 | 36 | 37 | 38 | )} 39 | {content} 40 | 41 | 42 | 43 | 44 | ) 45 | }, 46 | ) 47 | -------------------------------------------------------------------------------- /src/config.ts.example: -------------------------------------------------------------------------------- 1 | import { initializeApp } from "firebase/app"; 2 | import { getAuth } from "firebase/auth"; 3 | import { getDatabase } from "firebase/database"; 4 | 5 | 6 | export const firebaseConfig = { 7 | apiKey: "YOUR_API_KEY", 8 | authDomain: "YOUR_FIREBASE_AUTH_DOMAIN", 9 | databaseURL: "", 10 | projectId: "", 11 | storageBucket: "", 12 | messagingSenderId: "", 13 | appId: "", 14 | }; 15 | 16 | 17 | // Initialize Firebase 18 | const app = initializeApp(firebaseConfig); 19 | 20 | // Initialize Firebase services 21 | const firebaseAuth = getAuth(app); 22 | const firebaseDatabase = getDatabase(app); 23 | 24 | // Export the services 25 | export { firebaseAuth, firebaseDatabase }; -------------------------------------------------------------------------------- /src/context/Auth/authReducer.ts: -------------------------------------------------------------------------------- 1 | import { AuthState, ACTION_TYPE, AuthActionEnum } from "./types"; 2 | 3 | export const initialState: AuthState = { 4 | isAuthenticated: false, 5 | user: null, 6 | isLoading: true, 7 | }; 8 | 9 | const authReducer = (state: AuthState, action: ACTION_TYPE): AuthState => { 10 | switch (action.type) { 11 | case AuthActionEnum.SET_USER: 12 | return { 13 | ...state, 14 | isAuthenticated: true, 15 | user: action.payload, 16 | isLoading: false, 17 | }; 18 | case AuthActionEnum.LOGOUT: 19 | return { 20 | ...state, 21 | isAuthenticated: false, 22 | user: null, 23 | }; 24 | case AuthActionEnum.SET_LOADING: 25 | return { 26 | ...state, 27 | isLoading: action.payload, 28 | }; 29 | default: 30 | return state; 31 | } 32 | }; 33 | 34 | export default authReducer; 35 | -------------------------------------------------------------------------------- /src/context/Auth/index.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable react-refresh/only-export-components */ 2 | import { 3 | createContext, 4 | Dispatch, 5 | useContext, 6 | useEffect, 7 | useReducer, 8 | ReactNode, 9 | } from "react"; 10 | import { ACTION_TYPE, AuthActionEnum } from "./types"; 11 | import authReducer, { initialState } from "./authReducer"; 12 | import { onAuthStateChanged } from "firebase/auth"; 13 | import { firebaseAuth } from "@/config"; 14 | 15 | // Create context 16 | const AuthContextDispatch = createContext>(() => {}); 17 | const AuthContext = createContext(initialState); 18 | 19 | // AuthProvider 20 | const AuthProvider = ({ children }: { children: ReactNode }) => { 21 | const [state, dispatch] = useReducer(authReducer, initialState); 22 | 23 | useEffect(() => { 24 | dispatch({ type: AuthActionEnum.SET_LOADING, payload: true }); 25 | 26 | const unsubscribe = onAuthStateChanged(firebaseAuth, (currentUser) => { 27 | if (currentUser) { 28 | dispatch({ 29 | type: AuthActionEnum.SET_USER, 30 | payload: { 31 | email: currentUser.email || "", 32 | fullName: currentUser.displayName || "", 33 | profile_picture: currentUser.photoURL || "", 34 | uid: currentUser.uid, 35 | userName: currentUser.displayName || "", 36 | createdAt: new Date(currentUser.metadata.creationTime || '').getTime(), 37 | }, 38 | }); 39 | } 40 | dispatch({ type: AuthActionEnum.SET_LOADING, payload: false }); 41 | }); 42 | 43 | return () => unsubscribe(); 44 | }, [dispatch]); 45 | 46 | return ( 47 | 48 | 49 | {children} 50 | 51 | 52 | ); 53 | }; 54 | 55 | export const useAuth = () => useContext(AuthContext); 56 | export const useAuthDispatch = () => useContext(AuthContextDispatch); 57 | 58 | export default AuthProvider; 59 | -------------------------------------------------------------------------------- /src/context/Auth/types.ts: -------------------------------------------------------------------------------- 1 | export type User = { 2 | email: string; 3 | fullName: string; 4 | profile_picture: string; 5 | userName: string; 6 | uid: string; 7 | createdAt: number; 8 | } 9 | 10 | export type AuthState = { 11 | isAuthenticated: boolean; 12 | user: User | null; 13 | isLoading: boolean; 14 | } 15 | 16 | 17 | export enum AuthActionEnum { 18 | SET_LOADING = "SET_LOADING", 19 | LOGOUT = "LOGOUT", 20 | SET_USER = "SET_USER" 21 | } 22 | 23 | type SET_USER = { 24 | type: AuthActionEnum.SET_USER; 25 | payload: User; 26 | } 27 | 28 | type LOGOUT_USER = { 29 | type: AuthActionEnum.LOGOUT; 30 | payload?: null; 31 | } 32 | 33 | type SET_LOADING = { 34 | type: AuthActionEnum.SET_LOADING; 35 | payload: boolean; 36 | } 37 | 38 | export type ACTION_TYPE = SET_USER | LOGOUT_USER | SET_LOADING; 39 | 40 | -------------------------------------------------------------------------------- /src/context/Chat/chatReducer.ts: -------------------------------------------------------------------------------- 1 | import { DB_NAME } from "@/utils"; 2 | import { ACTION_TYPE, ChatState } from "./types"; 3 | 4 | export const initialState: ChatState = { 5 | userList: {}, 6 | isLoading: false, 7 | chatRoomId: DB_NAME.CHAT_ROOM, 8 | oneToOneRoomId: "", 9 | }; 10 | 11 | export const chatReducer = (state: ChatState, action: ACTION_TYPE) => { 12 | switch (action.type) { 13 | case "SET_USER_LIST": 14 | return { 15 | ...state, 16 | userList: action.payload, 17 | }; 18 | case "SET_LOADING": 19 | return { 20 | ...state, 21 | isLoading: action.payload, 22 | }; 23 | case "SET_CHAT_ROOM_ID": 24 | return { 25 | ...state, 26 | chatRoomId: action.payload.chatRoomId, 27 | oneToOneRoomId: action.payload.oneToOneRoomId, 28 | }; 29 | default: 30 | return state; 31 | } 32 | }; 33 | -------------------------------------------------------------------------------- /src/context/Chat/index.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable react-refresh/only-export-components */ 2 | import { createContext, Dispatch, useContext, useReducer } from "react"; 3 | import { ACTION_TYPE, ChatState } from "./types"; 4 | import { chatReducer, initialState } from "./chatReducer"; 5 | 6 | const chatDispatchContext = createContext>(() => {}); 7 | const chatContext = createContext(initialState); 8 | 9 | const ChatProvider = ({ children }: { children: React.ReactNode }) => { 10 | const [messages, dispatch] = useReducer(chatReducer, initialState); 11 | 12 | return ( 13 | 14 | 15 | {children} 16 | 17 | 18 | ); 19 | }; 20 | 21 | const useChat = () => useContext(chatContext); 22 | const useChatDispatch = () => useContext(chatDispatchContext); 23 | 24 | export { ChatProvider, useChat, useChatDispatch }; 25 | -------------------------------------------------------------------------------- /src/context/Chat/types.ts: -------------------------------------------------------------------------------- 1 | import { User } from "../Auth/types"; 2 | 3 | export type IMessage = { 4 | name: string; 5 | text: string; 6 | time: number; 7 | uid: string; 8 | key: string; 9 | }; 10 | 11 | export type MessageSnapshotResponse = { 12 | [key: string]: IMessage; 13 | }; 14 | 15 | export type ChatState = { 16 | userList: {[key: string]: User}; 17 | isLoading: boolean; 18 | chatRoomId: string; 19 | oneToOneRoomId: string; 20 | }; 21 | 22 | export enum ChatActionType { 23 | SET_USER_LIST = "SET_USER_LIST", 24 | SET_LOADING = "SET_LOADING", 25 | SET_CHAT_ROOM_ID = "SET_CHAT_ROOM_ID", 26 | } 27 | 28 | type SET_USER_LIST = { 29 | type: ChatActionType.SET_USER_LIST; 30 | payload: {[key: string]: User}; 31 | }; 32 | 33 | type SET_LOADING = { 34 | type: ChatActionType.SET_LOADING; 35 | payload: boolean; 36 | }; 37 | 38 | type SET_CHAT_ROOM_ID = { 39 | type: ChatActionType.SET_CHAT_ROOM_ID; 40 | payload: { 41 | chatRoomId: string; 42 | oneToOneRoomId: string; 43 | }; 44 | }; 45 | 46 | export type ACTION_TYPE = SET_USER_LIST | SET_LOADING | SET_CHAT_ROOM_ID; -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | .left__section { 2 | height: 80vh; 3 | transition: 0.8s all; 4 | overflow: scroll; 5 | scroll-behavior: smooth; 6 | overflow-x: hidden; 7 | } 8 | .right__section { 9 | height: 80vh; 10 | transition: 0.8s all; 11 | overflow: scroll; 12 | scroll-behavior: smooth; 13 | overflow-x: hidden; 14 | } 15 | 16 | ::-webkit-scrollbar { 17 | width: 2px; 18 | } 19 | 20 | ::-webkit-scrollbar-thumb { 21 | box-shadow: inset 0 0 5px rgba(253, 186, 116, 0.7); 22 | } 23 | -------------------------------------------------------------------------------- /src/main.tsx: -------------------------------------------------------------------------------- 1 | import { StrictMode } from 'react' 2 | import { createRoot } from 'react-dom/client' 3 | import './index.css' 4 | import App from './App.tsx' 5 | import { Provider } from './components/ui/provider.tsx' 6 | import AuthProvider from './context/Auth/index.tsx' 7 | 8 | // unregister service worker 9 | import './registerServiceWorker.ts' 10 | 11 | // firebase initialization 12 | import "@/config.ts"; 13 | 14 | createRoot(document.getElementById('root')!).render( 15 | 16 | 17 | 18 | 19 | 20 | 21 | , 22 | ) 23 | -------------------------------------------------------------------------------- /src/registerServiceWorker.ts: -------------------------------------------------------------------------------- 1 | // unregister previously registered service worker 2 | 3 | if ("serviceWorker" in navigator) { 4 | navigator.serviceWorker.getRegistrations().then(function (registrations) { 5 | for (let registration of registrations) { 6 | if ( 7 | registration.active && 8 | registration.active.scriptURL == 9 | "https://chatroom-67e21.web.app/service-worker.js" 10 | ) { 11 | registration.unregister(); 12 | } 13 | } 14 | }); 15 | } 16 | -------------------------------------------------------------------------------- /src/services/index.ts: -------------------------------------------------------------------------------- 1 | import { firebaseDatabase } from "@/config"; 2 | import { User } from "@/context/Auth/types"; 3 | import { MessageSnapshotResponse } from "@/context/Chat/types"; 4 | import { DB_NAME } from "@/utils"; 5 | import { 6 | child, 7 | get, 8 | limitToLast, 9 | off, 10 | query, 11 | ref, 12 | set, 13 | update, 14 | } from "firebase/database"; 15 | 16 | export const addUserInfo = async (user: User) => { 17 | const collectionRef = ref(firebaseDatabase, DB_NAME.USER_TABLE); 18 | const snapshot = await child(collectionRef, user.uid); 19 | if (!snapshot.key) { 20 | set(snapshot, user); 21 | return true; 22 | } 23 | update(snapshot, user); 24 | }; 25 | 26 | const fetchMessages = async ( 27 | collectionRef: ReturnType 28 | ): Promise => { 29 | const snapshot = await get(query(collectionRef, limitToLast(250))); 30 | return snapshot.exists() 31 | ? (snapshot.toJSON() as MessageSnapshotResponse) 32 | : {}; 33 | }; 34 | 35 | export const fetchCommonRoomMessages = 36 | async (): Promise => { 37 | const collectionRef = ref(firebaseDatabase, DB_NAME.CHAT_ROOM); 38 | // By default the fetch item is sorted by timestamp 39 | return fetchMessages(collectionRef); 40 | }; 41 | 42 | export const fetchOneToOneMessages = async ( 43 | senderRoomId: string, 44 | receiverRoomId: string 45 | ): Promise => { 46 | const senderMessages = await fetchMessages( 47 | ref(firebaseDatabase, senderRoomId) 48 | ); 49 | const receiverMessages = await fetchMessages( 50 | ref(firebaseDatabase, receiverRoomId) 51 | ); 52 | // Sort messages by timestamp 53 | const allMessages = Object.assign({}, senderMessages, receiverMessages); 54 | const sortedMessagesArray = Object.entries(allMessages) 55 | .sort(([, a], [, b]) => new Date(a.time).getTime() - new Date(b.time).getTime()); 56 | const sortedMessages = sortedMessagesArray.reduce((acc, [key, value]) => { 57 | acc[key] = value; 58 | return acc; 59 | }, {} as MessageSnapshotResponse); 60 | return sortedMessages; 61 | }; 62 | 63 | export const unSubscribeDatabase = (dbNames: string[]): boolean => { 64 | dbNames.forEach((dbName) => off(ref(firebaseDatabase, dbName))); 65 | return true; 66 | }; 67 | -------------------------------------------------------------------------------- /src/utils/emoji.ts: -------------------------------------------------------------------------------- 1 | export const emojis = [ 2 | '😄', 3 | '😃', 4 | '😀', 5 | '😊', 6 | '😉', 7 | '😍', 8 | '😘', 9 | '😚', 10 | '😗', 11 | '😙', 12 | '😜', 13 | '😝', 14 | '😛', 15 | '😳', 16 | '😁', 17 | '😔', 18 | '😌', 19 | '😒', 20 | '😞', 21 | '😣', 22 | '😢', 23 | '😂', 24 | '😭', 25 | '😪', 26 | '😥', 27 | '😰', 28 | '😅', 29 | '😓', 30 | '😩', 31 | '😫', 32 | '😨', 33 | '😱', 34 | '😠', 35 | '😡', 36 | '😤', 37 | '😖', 38 | '😆', 39 | '😋', 40 | '😷', 41 | '😎', 42 | '😴', 43 | '😵', 44 | '😲', 45 | '😟', 46 | '😦', 47 | '😧', 48 | '👿', 49 | '😮', 50 | '😬', 51 | '😐', 52 | '😕', 53 | '😯', 54 | '😏', 55 | '😑', 56 | '👲', 57 | '👳', 58 | '👮', 59 | '👷', 60 | '💂', 61 | '👶', 62 | '👦', 63 | '👧', 64 | '👨', 65 | '👩', 66 | '👴', 67 | '👵', 68 | '👱', 69 | '👼', 70 | '👸', 71 | '😺', 72 | '😸', 73 | '😻', 74 | '😽', 75 | '😼', 76 | '🙀', 77 | '😿', 78 | '😹', 79 | '😾', 80 | '👹', 81 | '👺', 82 | '🙈', 83 | '🙉', 84 | '🙊', 85 | '💀', 86 | '👽', 87 | '💩', 88 | '🔥', 89 | '✨', 90 | '🌟', 91 | '💫', 92 | '💥', 93 | '💢', 94 | '💦', 95 | '💧', 96 | '💤', 97 | '💨', 98 | '👂', 99 | '👀', 100 | '👃', 101 | '👅', 102 | '👄', 103 | '👍', 104 | '👎', 105 | '👌', 106 | '👊', 107 | '✊', 108 | '👋', 109 | '✋', 110 | '👐', 111 | '👆', 112 | '👇', 113 | '👉', 114 | '👈', 115 | '🙌', 116 | '🙏', 117 | '👏', 118 | '💪', 119 | '🚶', 120 | '🏃', 121 | '💃', 122 | '👫', 123 | '👪', 124 | '💏', 125 | '💑', 126 | '👯', 127 | '🙆', 128 | '🙅', 129 | '💁', 130 | '🙋', 131 | '💆', 132 | '💇', 133 | '💅', 134 | '👰', 135 | '🙎', 136 | '🙍', 137 | '🙇', 138 | '🎩', 139 | '👑', 140 | '👒', 141 | '👟', 142 | '👞', 143 | '👡', 144 | '👠', 145 | '👢', 146 | '👕', 147 | '👔', 148 | '👚', 149 | '👗', 150 | '🎽', 151 | '👖', 152 | '👘', 153 | '👙', 154 | '💼', 155 | '👜', 156 | '👝', 157 | '👛', 158 | '👓', 159 | '🎀', 160 | '🌂', 161 | '💄', 162 | '💛', 163 | '💙', 164 | '💜', 165 | '💚', 166 | '💔', 167 | '💗', 168 | '💓', 169 | '💕', 170 | '💖', 171 | '💞', 172 | '💘', 173 | '💌', 174 | '💋', 175 | '💍', 176 | '💎', 177 | '👤', 178 | '💬', 179 | '👣', 180 | ] -------------------------------------------------------------------------------- /src/utils/index.ts: -------------------------------------------------------------------------------- 1 | export const dateFormatter = new Intl.DateTimeFormat(navigator.language, { 2 | month: "short", 3 | day: "numeric", 4 | year: "numeric", 5 | hour: "numeric", 6 | minute: "2-digit", 7 | }); 8 | 9 | export const dateFormatterShort = new Intl.DateTimeFormat(navigator.language, { 10 | month: "short", 11 | day: "numeric", 12 | year: "numeric", 13 | }); 14 | 15 | export const numberFormatter = new Intl.NumberFormat(navigator.language, { 16 | notation: "compact", 17 | }); 18 | 19 | export const DB_NAME: { [key: string]: string } = { 20 | USER_TABLE: "usersTable", 21 | CHAT_ROOM: "chatRoom", 22 | }; 23 | -------------------------------------------------------------------------------- /src/utils/useDebounce.ts: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from "react"; 2 | 3 | const useDebounce = (value: T, delay = 500) => { 4 | const [debouncedValue, setDebouncedValue] = useState(value); 5 | 6 | useEffect(() => { 7 | const timer = setTimeout(() => { 8 | setDebouncedValue(value); 9 | }, delay); 10 | 11 | return () => clearTimeout(timer); 12 | }, [value, delay]); 13 | 14 | return debouncedValue; 15 | }; 16 | 17 | export default useDebounce; 18 | -------------------------------------------------------------------------------- /src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", 4 | "target": "ES2020", 5 | "useDefineForClassFields": true, 6 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 7 | "module": "ESNext", 8 | "skipLibCheck": true, 9 | 10 | /* Bundler mode */ 11 | "moduleResolution": "bundler", 12 | "allowImportingTsExtensions": true, 13 | "isolatedModules": true, 14 | "moduleDetection": "force", 15 | "noEmit": true, 16 | "jsx": "react-jsx", 17 | 18 | /* Linting */ 19 | "strict": true, 20 | "noUnusedLocals": true, 21 | "noUnusedParameters": true, 22 | "noFallthroughCasesInSwitch": true, 23 | "noUncheckedSideEffectImports": true, 24 | "baseUrl": ".", 25 | "paths": { 26 | "@/*": ["./src/*"] 27 | } 28 | }, 29 | "include": ["src"] 30 | } 31 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": [], 3 | "references": [ 4 | { "path": "./tsconfig.app.json" }, 5 | { "path": "./tsconfig.node.json" } 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", 4 | "target": "ES2022", 5 | "lib": ["ES2023"], 6 | "module": "ESNext", 7 | "skipLibCheck": true, 8 | 9 | /* Bundler mode */ 10 | "moduleResolution": "bundler", 11 | "allowImportingTsExtensions": true, 12 | "isolatedModules": true, 13 | "moduleDetection": "force", 14 | "noEmit": true, 15 | 16 | /* Linting */ 17 | "strict": true, 18 | "noUnusedLocals": true, 19 | "noUnusedParameters": true, 20 | "noFallthroughCasesInSwitch": true, 21 | "noUncheckedSideEffectImports": true, 22 | "baseUrl": ".", 23 | "paths": { 24 | "@/*": ["./src/*"] 25 | } 26 | }, 27 | "include": ["vite.config.ts"] 28 | } 29 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import react from '@vitejs/plugin-react' 3 | import tsconfigPaths from "vite-tsconfig-paths" 4 | 5 | // https://vite.dev/config/ 6 | export default defineConfig({ 7 | plugins: [react(), tsconfigPaths()], 8 | }) 9 | --------------------------------------------------------------------------------