├── .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 | 
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 |
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 |
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 |
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 |
43 |
44 |
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 |
--------------------------------------------------------------------------------