├── src ├── react-app-env.d.ts ├── assets │ ├── adam.png │ ├── bob.png │ ├── callum.png │ ├── daniel.png │ ├── valley.jpeg │ ├── VisbyRoundCF-Heavy.woff │ ├── VisbyRoundCF-DemiBold.woff │ └── VisbyRoundCF-Regular.woff ├── functions │ ├── dates.tsx │ ├── constants.tsx │ ├── getOtherUser.tsx │ ├── isMobile.tsx │ └── context.tsx ├── index.tsx ├── App.tsx ├── AuthPage │ ├── components │ │ ├── Link.tsx │ │ ├── Button.tsx │ │ ├── TextInput.tsx │ │ └── PhotoInput.tsx │ ├── index.tsx │ ├── LogInForm.tsx │ └── SignUpForm.tsx ├── ChatsPage │ ├── Sidebar.tsx │ ├── MessageForm.tsx │ ├── ChatCard.tsx │ ├── UserSearch.tsx │ ├── index.tsx │ └── ChatHeader.tsx ├── app.css └── theme.css ├── public ├── robots.txt ├── favicon.ico ├── logo192.png ├── logo512.png ├── manifest.json └── index.html ├── .gitignore ├── tsconfig.json ├── package.json └── README.md /src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alamorre/pretty-chat-react-typescript/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alamorre/pretty-chat-react-typescript/HEAD/public/logo192.png -------------------------------------------------------------------------------- /public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alamorre/pretty-chat-react-typescript/HEAD/public/logo512.png -------------------------------------------------------------------------------- /src/assets/adam.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alamorre/pretty-chat-react-typescript/HEAD/src/assets/adam.png -------------------------------------------------------------------------------- /src/assets/bob.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alamorre/pretty-chat-react-typescript/HEAD/src/assets/bob.png -------------------------------------------------------------------------------- /src/assets/callum.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alamorre/pretty-chat-react-typescript/HEAD/src/assets/callum.png -------------------------------------------------------------------------------- /src/assets/daniel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alamorre/pretty-chat-react-typescript/HEAD/src/assets/daniel.png -------------------------------------------------------------------------------- /src/assets/valley.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alamorre/pretty-chat-react-typescript/HEAD/src/assets/valley.jpeg -------------------------------------------------------------------------------- /src/assets/VisbyRoundCF-Heavy.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alamorre/pretty-chat-react-typescript/HEAD/src/assets/VisbyRoundCF-Heavy.woff -------------------------------------------------------------------------------- /src/assets/VisbyRoundCF-DemiBold.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alamorre/pretty-chat-react-typescript/HEAD/src/assets/VisbyRoundCF-DemiBold.woff -------------------------------------------------------------------------------- /src/assets/VisbyRoundCF-Regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alamorre/pretty-chat-react-typescript/HEAD/src/assets/VisbyRoundCF-Regular.woff -------------------------------------------------------------------------------- /src/functions/dates.tsx: -------------------------------------------------------------------------------- 1 | export const nowTimeStamp = () => { 2 | return new Date() 3 | .toISOString() 4 | .replace("T", " ") 5 | .replace("Z", `${Math.floor(Math.random() * 1000)}+00:00`); 6 | }; 7 | -------------------------------------------------------------------------------- /src/functions/constants.tsx: -------------------------------------------------------------------------------- 1 | export const projectId = process.env.REACT_APP_PROJECT_ID 2 | ? process.env.REACT_APP_PROJECT_ID 3 | : ""; 4 | 5 | export const privateKey: string = process.env.REACT_APP_PROJECT_KEY 6 | ? process.env.REACT_APP_PROJECT_KEY 7 | : ""; 8 | -------------------------------------------------------------------------------- /src/functions/getOtherUser.tsx: -------------------------------------------------------------------------------- 1 | import { ChatObject, PersonObject } from "react-chat-engine-advanced"; 2 | 3 | export const getOtherUser = ( 4 | chat: ChatObject, 5 | username: string 6 | ): PersonObject | undefined => { 7 | const otherMember = chat.people.find( 8 | (member) => member.person.username !== username 9 | ); 10 | return otherMember?.person; 11 | }; 12 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import ReactDOM from "react-dom/client"; 2 | import App from "./App"; 3 | 4 | import "./assets/VisbyRoundCF-Regular.woff"; 5 | 6 | import { ContextProvider } from "./functions/context"; 7 | 8 | const root = ReactDOM.createRoot( 9 | document.getElementById("root") as HTMLElement 10 | ); 11 | root.render( 12 | 13 | 14 | 15 | ); 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | import { useContext } from "react"; 2 | 3 | import { Context } from "./functions/context"; 4 | 5 | import AuthPage from "./AuthPage"; 6 | import ChatsPage from "./ChatsPage"; 7 | 8 | import "./app.css"; 9 | 10 | function App() { 11 | const { user } = useContext(Context); 12 | 13 | if (user) { 14 | return ; 15 | } else { 16 | return ; 17 | } 18 | } 19 | 20 | export default App; 21 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /src/functions/isMobile.tsx: -------------------------------------------------------------------------------- 1 | import { useLayoutEffect, useState } from "react"; 2 | 3 | // TODO: Copy paste this: https://www.codegrepper.com/code-examples/javascript/react+get+window+width+on+resize 4 | export const useIsMobile = () => { 5 | const [size, setSize] = useState([0, 0]); 6 | useLayoutEffect(() => { 7 | function updateSize() { 8 | setSize([window.innerWidth, window.innerHeight]); 9 | } 10 | window.addEventListener("resize", updateSize); 11 | updateSize(); 12 | return () => window.removeEventListener("resize", updateSize); 13 | }, []); 14 | return size[0] < 820; 15 | }; 16 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "esModuleInterop": true, 12 | "allowSyntheticDefaultImports": true, 13 | "strict": true, 14 | "forceConsistentCasingInFileNames": true, 15 | "noFallthroughCasesInSwitch": true, 16 | "module": "esnext", 17 | "moduleResolution": "node", 18 | "resolveJsonModule": true, 19 | "isolatedModules": true, 20 | "noEmit": true, 21 | "jsx": "react-jsx" 22 | }, 23 | "include": [ 24 | "src" 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /src/functions/context.tsx: -------------------------------------------------------------------------------- 1 | import { useState, createContext, ReactNode } from "react"; 2 | import { PersonObject } from "react-chat-engine-advanced"; 3 | 4 | export interface ContextInterface { 5 | user: PersonObject | undefined; 6 | setUser: (u: PersonObject | undefined) => void; 7 | } 8 | 9 | interface ProviderInterface { 10 | children: ReactNode; 11 | } 12 | 13 | export const Context = createContext({ 14 | user: undefined, 15 | setUser: () => {}, 16 | }); 17 | 18 | export const ContextProvider = (props: ProviderInterface) => { 19 | const [user, setUser] = useState(undefined); 20 | const value = { user, setUser }; 21 | 22 | return {props.children}; 23 | }; 24 | -------------------------------------------------------------------------------- /src/AuthPage/components/Link.tsx: -------------------------------------------------------------------------------- 1 | import { CSSProperties, ReactNode, useState } from "react"; 2 | 3 | interface LinkProps { 4 | children?: ReactNode; 5 | style?: CSSProperties; 6 | type?: string; 7 | onClick?: () => void; 8 | } 9 | 10 | const Link = (props: LinkProps) => { 11 | const [hovered, setHovered] = useState(false); 12 | 13 | return ( 14 | setHovered(true)} 17 | onMouseLeave={() => setHovered(false)} 18 | style={{ 19 | ...styles.style, 20 | ...(hovered && styles.hoverStyle), 21 | ...props.style, 22 | }} 23 | > 24 | {props.children} 25 | 26 | ); 27 | }; 28 | 29 | const styles = { 30 | style: { 31 | color: "#fa541c", 32 | cursor: "pointer", 33 | transition: "all .44s ease", 34 | WebkitTransition: "all .44s ease", 35 | MozTransition: "all .44s ease", 36 | } as CSSProperties, 37 | hoverStyle: { 38 | filter: "brightness(145%)", 39 | textDecoration: "underline", 40 | } as CSSProperties, 41 | }; 42 | 43 | export default Link; 44 | -------------------------------------------------------------------------------- /src/ChatsPage/Sidebar.tsx: -------------------------------------------------------------------------------- 1 | import { useContext } from "react"; 2 | 3 | import { 4 | LogoutOutlined, 5 | HomeFilled, 6 | MessageFilled, 7 | SettingFilled, 8 | } from "@ant-design/icons"; 9 | 10 | import { Avatar } from "react-chat-engine-advanced"; 11 | 12 | import { Context } from "../functions/context"; 13 | 14 | const Sidebar = () => { 15 | const { user, setUser } = useContext(Context); 16 | 17 | return ( 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 31 | 32 | setUser(undefined)} 34 | className="signout-icon" 35 | /> 36 | 37 | ); 38 | }; 39 | 40 | export default Sidebar; 41 | -------------------------------------------------------------------------------- /src/AuthPage/components/Button.tsx: -------------------------------------------------------------------------------- 1 | import { CSSProperties, ReactNode, useState } from "react"; 2 | 3 | interface ButtonProps { 4 | children?: ReactNode; 5 | style?: CSSProperties; 6 | type?: string; 7 | } 8 | 9 | const Button = (props: ButtonProps) => { 10 | const [hovered, setHovered] = useState(false); 11 | 12 | return ( 13 | setHovered(true)} 15 | onMouseLeave={() => setHovered(false)} 16 | style={{ 17 | ...styles.style, 18 | ...(hovered && styles.hoverStyle), 19 | ...props.style, 20 | }} 21 | > 22 | {props.children} 23 | 24 | ); 25 | }; 26 | 27 | const styles = { 28 | style: { 29 | width: "100%", 30 | height: "53px", 31 | color: "white", 32 | backgroundColor: "#fa541c", 33 | border: "none", 34 | outline: "none", 35 | borderRadius: "8px", 36 | fontFamily: "VisbyRoundCF-DemiBold", 37 | cursor: "pointer", 38 | transition: "all .44s ease", 39 | WebkitTransition: "all .44s ease", 40 | MozTransition: "all .44s ease", 41 | } as CSSProperties, 42 | hoverStyle: { filter: "brightness(145%)" } as CSSProperties, 43 | }; 44 | 45 | export default Button; 46 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pretty-chat-react-typescript", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@testing-library/jest-dom": "^5.14.1", 7 | "@testing-library/react": "^13.0.0", 8 | "@testing-library/user-event": "^13.2.1", 9 | "@types/jest": "^27.0.1", 10 | "@types/node": "^16.7.13", 11 | "@types/react": "^18.0.0", 12 | "@types/react-dom": "^18.0.0", 13 | "antd": "4.21.5", 14 | "axios": "^0.27.2", 15 | "react": "^18.2.0", 16 | "react-chat-engine-advanced": "0.1.21", 17 | "react-dom": "^18.2.0", 18 | "react-scripts": "5.0.1", 19 | "typescript": "^4.4.2", 20 | "web-vitals": "^2.1.0" 21 | }, 22 | "scripts": { 23 | "start": "react-scripts start", 24 | "build": "react-scripts build", 25 | "test": "react-scripts test", 26 | "eject": "react-scripts eject" 27 | }, 28 | "eslintConfig": { 29 | "extends": [ 30 | "react-app", 31 | "react-app/jest" 32 | ] 33 | }, 34 | "browserslist": { 35 | "production": [ 36 | ">0.2%", 37 | "not dead", 38 | "not op_mini all" 39 | ], 40 | "development": [ 41 | "last 1 chrome version", 42 | "last 1 firefox version", 43 | "last 1 safari version" 44 | ] 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/AuthPage/index.tsx: -------------------------------------------------------------------------------- 1 | import { CSSProperties, useState } from "react"; 2 | 3 | import valley from "../assets/valley.jpeg"; 4 | import SignUpForm from "./SignUpForm"; 5 | import LogInForm from "./LogInForm"; 6 | 7 | const AuthPage = () => { 8 | const [hasAccount, setHasAccount] = useState(false); 9 | 10 | const backgroundImage = { 11 | backgroundImage: `url(${valley})`, // Here due to variable 12 | } as CSSProperties; 13 | 14 | return ( 15 | 16 | 17 | 18 | Pretty 19 | 20 | {hasAccount ? ( 21 | setHasAccount(false)} /> 22 | ) : ( 23 | setHasAccount(true)} /> 24 | )} 25 | 26 | 27 | 28 | ); 29 | }; 30 | 31 | const styles = { 32 | formContainerStyle: { 33 | width: "100%", 34 | maxWidth: "650px", 35 | padding: "36px 72px", 36 | } as CSSProperties, 37 | titleStyle: { 38 | fontSize: "24px", 39 | fontFamily: "VisbyRoundCF-Heavy", 40 | letterSpacing: "0.5px", 41 | color: "white", 42 | paddingBottom: "11vw", 43 | } as CSSProperties, 44 | }; 45 | 46 | export default AuthPage; 47 | -------------------------------------------------------------------------------- /src/ChatsPage/MessageForm.tsx: -------------------------------------------------------------------------------- 1 | import { useContext, useState } from "react"; 2 | 3 | import { CaretUpFilled } from "@ant-design/icons"; 4 | 5 | import { MessageObject, MessageFormProps } from "react-chat-engine-advanced"; 6 | 7 | import { nowTimeStamp } from "../functions/dates"; 8 | import { Context } from "../functions/context"; 9 | 10 | const MessageForm = (props: MessageFormProps) => { 11 | const [text, setText] = useState(""); 12 | const { user } = useContext(Context); 13 | 14 | const onSubmit = (event: React.FormEvent) => { 15 | event.preventDefault(); 16 | 17 | if (text.trim().length === 0) { 18 | return; 19 | } 20 | if (!user || user.email === null) { 21 | return; 22 | } 23 | 24 | setText(""); 25 | 26 | const message: MessageObject = { 27 | text: text, 28 | sender_username: user.email, 29 | created: nowTimeStamp(), 30 | custom_json: {}, 31 | attachments: [], 32 | }; 33 | 34 | props.onSubmit && props.onSubmit(message); 35 | }; 36 | 37 | return ( 38 | 39 | setText(e.target.value)} 41 | value={text} 42 | placeholder="Type something..." 43 | className="ce-custom-message-input" 44 | /> 45 | 46 | 47 | 48 | 49 | 50 | ); 51 | }; 52 | 53 | export default MessageForm; 54 | -------------------------------------------------------------------------------- /src/AuthPage/components/TextInput.tsx: -------------------------------------------------------------------------------- 1 | import { CSSProperties, ChangeEventHandler } from "react"; 2 | 3 | interface TextInputProps { 4 | label: string; 5 | name: string; 6 | type?: string; 7 | placeholder?: string; 8 | style?: CSSProperties; 9 | onChange: ChangeEventHandler; 10 | } 11 | 12 | const TextInput = (props: TextInputProps) => { 13 | return ( 14 | 20 | {props.label} 21 | 22 | 29 | 30 | ); 31 | }; 32 | 33 | const styles = { 34 | style: { 35 | position: "relative", 36 | display: "inline-block", 37 | width: "100%", 38 | paddingBottom: "12px", 39 | } as CSSProperties, 40 | labelStyle: { 41 | position: "absolute", 42 | top: "8px", 43 | left: "18px", 44 | fontSize: "11px", 45 | color: "rgb(175, 175, 175)", 46 | fontFamily: "VisbyRoundCF-DemiBold", 47 | width: "100px", 48 | } as CSSProperties, 49 | inputStyle: { 50 | backgroundColor: "#3e404b", 51 | color: "white", 52 | fontFamily: "VisbyRoundCF-DemiBold", 53 | outline: "none", 54 | border: "none", 55 | borderRadius: "8px", 56 | padding: "24px 18px 12px 18px", 57 | width: "100%", // For the padding 18px + 18px 58 | } as CSSProperties, 59 | }; 60 | 61 | export default TextInput; 62 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Pretty Chat React/Typescript 2 | 3 | This project is a pretty full-stack chat app built with React and Typesctipt. 4 | 5 | To learn how this project works, watch the following YouTube tutorial. 6 | 7 | ## Setup 8 | 9 | Go to [chatengine.io](https://chatengine.io) and create your own project. There you will get a Project ID and Private Key which are needed for user signup and authentication. 10 | 11 | ### `.env.local` 12 | 13 | Create a `.env.local` file at the top-level of your project, and replace the UUIDs with your own Project ID and Private Key from [chatengine.io](https://chatengine.io). 14 | 15 | ``` 16 | REACT_APP_PROJECT_ID=12341234-1234-1234-1234-123412341234 17 | REACT_APP_PROJECT_KEY=abcdabcd-abcd-abcd-abcd-abcdabcdabcd 18 | ``` 19 | 20 | This will link your new React App to the right Chat Engine project. 21 | 22 | ### `npm install` 23 | 24 | Build out your node modules by running `npm install`. Then you cn start the app. 25 | 26 | ### `npm run start` 27 | 28 | This will start the app. By default it will run on [localhost:3000](http://localhost:3000/) 29 | 30 | ### `npm run build` 31 | 32 | Builds the app for production to the `build` folder.\ 33 | It correctly bundles React in production mode and optimizes the build for the best performance. 34 | 35 | The build is minified and the filenames include the hashes.\ 36 | Your app is ready to be deployed! 37 | 38 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. 39 | 40 | ## Learn More 41 | 42 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). 43 | 44 | To learn React, check out the [React documentation](https://reactjs.org/). 45 | -------------------------------------------------------------------------------- /src/ChatsPage/ChatCard.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | ChatCard, 3 | ChatCardProps, 4 | ChatObject, 5 | } from "react-chat-engine-advanced"; 6 | 7 | import { getOtherUser } from "../functions/getOtherUser"; 8 | 9 | interface CustomChatCardProps extends ChatCardProps { 10 | username: string; 11 | isActive: boolean; 12 | onChatCardClick: (chatId: number) => void; 13 | chat?: ChatObject; 14 | } 15 | 16 | const CustomChatCard = (props: CustomChatCardProps) => { 17 | if (!props.chat) return ; 18 | 19 | const otherMember = getOtherUser(props.chat, props.username); 20 | const firstName = otherMember ? otherMember.first_name : ""; 21 | const lastName = otherMember ? otherMember.last_name : ""; 22 | const username = otherMember ? otherMember.username : ""; 23 | const messageText = props.chat.last_message.text; 24 | const hasNotification = 25 | props.chat.last_message.sender_username !== props.username; 26 | 27 | return ( 28 | props.chat && props.onChatCardClick(props.chat.id)} 48 | /> 49 | ); 50 | }; 51 | 52 | export default CustomChatCard; 53 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | React App 28 | 29 | 30 | You need to enable JavaScript to run this app. 31 | 32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /src/app.css: -------------------------------------------------------------------------------- 1 | @import url("https://cdnjs.cloudflare.com/ajax/libs/antd/4.21.5/antd.min.css"); 2 | 3 | @font-face { 4 | /* TODO: Font Family https://www.dfonts.org/assets/visby-round-font-family/ */ 5 | font-family: 'VisbyRoundCF-Regular'; 6 | src: local('VisbyRoundCF-Regular'), url(./assets/VisbyRoundCF-Regular.woff) format('woff'); 7 | font-weight: normal; 8 | } 9 | 10 | @font-face { 11 | /* TODO: Font Family https://www.dfonts.org/assets/visby-round-font-family/ */ 12 | font-family: 'VisbyRoundCF-DemiBold'; 13 | src: local('VisbyRoundCF-DemiBold'), url(./assets/VisbyRoundCF-DemiBold.woff) format('woff'); 14 | font-weight: normal; 15 | } 16 | 17 | @font-face { 18 | /* TODO: Font Family https://www.dfonts.org/assets/visby-round-font-family/ */ 19 | font-family: 'VisbyRoundCF-Heavy'; 20 | src: local('VisbyRoundCF-Heavy'), url(./assets/VisbyRoundCF-Heavy.woff) format('woff'); 21 | font-weight: normal; 22 | } 23 | 24 | body { 25 | font-family: 'VisbyRoundCF-Regular'; 26 | margin: 0px; 27 | } 28 | 29 | .form-title { 30 | font-size: 42px; 31 | font-family: 'VisbyRoundCF-Heavy'; 32 | letter-spacing: 0.5px; 33 | color: #e8e8e8; 34 | padding-bottom: 12px; 35 | } 36 | 37 | .form-subtitle { 38 | font-size: 18px; 39 | font-family: 'VisbyRoundCF-Regular'; 40 | letter-spacing: 0.5px; 41 | color: #afafaf; 42 | padding-bottom: 24px; 43 | } 44 | 45 | .background-image { 46 | width: 100vw; 47 | height: 100vh; 48 | background-repeat: no-repeat; 49 | background-size: cover; 50 | } 51 | 52 | .background-gradient-dark { 53 | width: 100vw; 54 | height: 100%; 55 | overflow-y: scroll; 56 | background: linear-gradient(66deg, rgb(40,43,54) 0%, rgb(40,43,54) 50%, rgba(40,43,54,0.8) 100%); 57 | /* ^ Built with https://cssgradient.io/ */ 58 | } 59 | 60 | .background-gradient-light { 61 | width: 100vw; 62 | height: 100%; 63 | overflow-y: scroll; 64 | background: linear-gradient(66deg, rgba(150, 157, 166, 0.9) 0%, rgba(150, 157, 166, 0.8) 50%, rgba(150,157,166,0.7) 100%); 65 | /* ^ Built with https://cssgradient.io/ */ 66 | } 67 | -------------------------------------------------------------------------------- /src/AuthPage/LogInForm.tsx: -------------------------------------------------------------------------------- 1 | import { useState, useContext } from "react"; 2 | 3 | import axios from "axios"; 4 | 5 | import TextInput from "./components/TextInput"; 6 | import Button from "./components/Button"; 7 | import Link from "./components/Link"; 8 | 9 | import { Context } from "../functions/context"; 10 | import { projectId } from "../functions/constants"; 11 | import { PersonObject } from "react-chat-engine-advanced"; 12 | 13 | interface LogInFormProps { 14 | onHasNoAccount: () => void; 15 | } 16 | 17 | const LogInForm = (props: LogInFormProps) => { 18 | // State 19 | const [email, setEmail] = useState(""); 20 | const [password, setPassword] = useState(""); 21 | // Hooks 22 | const { setUser } = useContext(Context); 23 | 24 | const onSubmit = (event: React.FormEvent) => { 25 | event.preventDefault(); 26 | 27 | const headers = { 28 | "Project-ID": projectId, 29 | "User-Name": email, 30 | "User-Secret": password, 31 | }; 32 | 33 | axios 34 | .get("https://api.chatengine.io/users/me/", { 35 | headers, 36 | }) 37 | .then((r) => { 38 | if (r.status === 200) { 39 | const user: PersonObject = { 40 | first_name: r.data.first_name, 41 | last_name: r.data.last_name, 42 | email: email, 43 | username: email, 44 | secret: password, 45 | avatar: r.data.avatar, 46 | custom_json: {}, 47 | is_online: true, 48 | }; 49 | setUser(user); 50 | } 51 | }) 52 | .catch((e) => console.log("Error", e)); 53 | }; 54 | 55 | return ( 56 | 57 | Welcome Back 58 | 59 | 60 | New here? props.onHasNoAccount()}>Sign Up 61 | 62 | 63 | 64 | setEmail(e.target.value)} 69 | /> 70 | 71 | setPassword(e.target.value)} 77 | /> 78 | 79 | Log In 80 | 81 | 82 | ); 83 | }; 84 | 85 | export default LogInForm; 86 | -------------------------------------------------------------------------------- /src/AuthPage/components/PhotoInput.tsx: -------------------------------------------------------------------------------- 1 | import { CSSProperties, ChangeEventHandler, useState } from "react"; 2 | 3 | interface PhotoInputProps { 4 | label: string; 5 | id: string; 6 | name: string; 7 | placeholder?: string; 8 | style?: CSSProperties; 9 | onChange: ChangeEventHandler; 10 | } 11 | 12 | const PhotoInput = (props: PhotoInputProps) => { 13 | const [selectedImage, setSelectedImage] = useState( 14 | undefined 15 | ); 16 | 17 | const onChange: ChangeEventHandler = (event) => { 18 | if (event.target.files !== null) { 19 | const image = event.target.files[0]; 20 | setSelectedImage(image); 21 | props.onChange(event); 22 | } 23 | }; 24 | 25 | return ( 26 | 32 | 40 | {selectedImage ? ( 41 | 51 | ) : ( 52 | + 53 | )} 54 | 55 | {props.label} 56 | 57 | 58 | 66 | 67 | ); 68 | }; 69 | 70 | const styles = { 71 | style: { 72 | position: "relative", 73 | display: "inline-block", 74 | width: "100%", 75 | paddingBottom: "12px", 76 | } as CSSProperties, 77 | uploadStyle: { 78 | color: "white", 79 | backgroundColor: "rgb(62, 64, 75)", 80 | display: "inline-block", 81 | height: "52px", 82 | width: "52px", 83 | borderRadius: "8px", 84 | textAlign: "center", 85 | fontSize: "30px", 86 | } as CSSProperties, 87 | labelStyle: { 88 | display: "inline-block", 89 | color: "rgb(175, 175, 175)", 90 | fontFamily: "VisbyRoundCF-DemiBold", 91 | fontSize: "14px", 92 | cursor: "pointer", 93 | position: "absolute", 94 | top: "18px", 95 | left: "66px", 96 | } as CSSProperties, 97 | inputStyle: { 98 | backgroundColor: "#3e404b", 99 | color: "white", 100 | fontFamily: "VisbyRoundCF-DemiBold", 101 | outline: "none", 102 | border: "none", 103 | borderRadius: "8px", 104 | padding: "24px 18px 12px 18px", 105 | width: "100%", // For the padding 18px + 18px 106 | } as CSSProperties, 107 | }; 108 | 109 | export default PhotoInput; 110 | -------------------------------------------------------------------------------- /src/ChatsPage/UserSearch.tsx: -------------------------------------------------------------------------------- 1 | import { AutoComplete, Input } from "antd"; 2 | import type { SelectProps } from "antd/es/select"; 3 | import { useState, useEffect, useRef } from "react"; 4 | 5 | import { PersonObject, Avatar } from "react-chat-engine-advanced"; 6 | 7 | import axios from "axios"; 8 | 9 | import { privateKey, projectId } from "../functions/constants"; 10 | 11 | interface CustomChatFormProps { 12 | username: string; 13 | secret: string; 14 | onSelect: (chatId: number) => void; 15 | } 16 | 17 | const UserSearch = (props: CustomChatFormProps) => { 18 | const didMountRef = useRef(false); 19 | 20 | const [loading, setLoading] = useState(false); 21 | const [query, setQuery] = useState(""); 22 | const [users, setUsers] = useState([]); 23 | const [options, setOptions] = useState["options"]>([]); 24 | 25 | useEffect(() => { 26 | if (!didMountRef.current) { 27 | didMountRef.current = true; 28 | const headers = { "Private-Key": privateKey }; 29 | axios 30 | .get("https://api.chatengine.io/users/", { headers }) 31 | .then((r) => setUsers(r.data)) 32 | .catch(); 33 | } 34 | }); 35 | 36 | const searchResult = (query: string) => { 37 | const foundUsers = users.filter( 38 | (user) => 39 | JSON.stringify(user).toLowerCase().indexOf(query.toLowerCase()) !== 40 | -1 && user.username !== props.username 41 | ); 42 | 43 | return foundUsers.map((user) => { 44 | return { 45 | value: user.username, 46 | label: ( 47 | 53 | 54 | 55 | 56 | 57 | 58 | {user.first_name} {user.last_name} 59 | 60 | {user.username} 61 | 62 | 63 | ), 64 | }; 65 | }); 66 | }; 67 | const handleSearch = (query: string) => { 68 | setOptions(query ? searchResult(query) : []); 69 | }; 70 | 71 | const onSelect = (value: string) => { 72 | setLoading(true); 73 | 74 | const headers = { 75 | "Project-ID": projectId, 76 | "User-Name": props.username, 77 | "User-Secret": props.secret, 78 | }; 79 | const data = { 80 | usernames: [props.username, value], 81 | }; 82 | axios 83 | .put("https://api.chatengine.io/chats/", data, { headers }) 84 | .then((r) => { 85 | props.onSelect(r.data.id); 86 | setLoading(false); 87 | setQuery(""); 88 | }) 89 | .catch(() => setLoading(false)); 90 | }; 91 | 92 | return ( 93 | 94 | 102 | setQuery(e.target.value)} 108 | /> 109 | 110 | 111 | ); 112 | }; 113 | 114 | export default UserSearch; 115 | -------------------------------------------------------------------------------- /src/ChatsPage/index.tsx: -------------------------------------------------------------------------------- 1 | import { useContext, CSSProperties } from "react"; 2 | 3 | import valley from "../assets/valley.jpeg"; 4 | 5 | import { useIsMobile } from "../functions/isMobile"; 6 | import { Context } from "../functions/context"; 7 | 8 | import { 9 | MultiChatWindow, 10 | MultiChatSocket, 11 | useMultiChatLogic, 12 | MessageFormProps, 13 | ChatCardProps, 14 | ChatHeaderProps, 15 | } from "react-chat-engine-advanced"; 16 | 17 | import "../theme.css"; 18 | 19 | import Sidebar from "./Sidebar"; 20 | import MessageForm from "./MessageForm"; 21 | import UserSearch from "./UserSearch"; 22 | import ChatCard from "./ChatCard"; 23 | import ChatHeader from "./ChatHeader"; 24 | 25 | import { projectId } from "../functions/constants"; 26 | 27 | const ChatsPage = () => { 28 | // Hooks 29 | const { user } = useContext(Context); 30 | const isMobile: boolean = useIsMobile(); 31 | 32 | // Chat Engine Hooks 33 | const username: string = user ? user.username : ""; 34 | const secret: string = user && user.secret !== null ? user.secret : ""; 35 | const chatProps = useMultiChatLogic(projectId, username, secret); 36 | 37 | const backgroundImage = { 38 | backgroundImage: `url(${valley})`, // Here due to variable 39 | } as CSSProperties; 40 | 41 | return ( 42 | 43 | 44 | 54 | 64 | 65 | 66 | 67 | 76 | 77 | 78 | ( 81 | 85 | chatProps.onChatCardClick(chatId) 86 | } 87 | /> 88 | )} 89 | renderChatCard={(props: ChatCardProps) => ( 90 | 100 | )} 101 | renderChatHeader={(props: ChatHeaderProps) => ( 102 | 108 | )} 109 | renderMessageForm={(props: MessageFormProps) => ( 110 | 111 | )} 112 | renderChatSettings={() => } 113 | style={{ height: "100%" }} 114 | /> 115 | 116 | 117 | 118 | 119 | ); 120 | }; 121 | 122 | export default ChatsPage; 123 | -------------------------------------------------------------------------------- /src/AuthPage/SignUpForm.tsx: -------------------------------------------------------------------------------- 1 | import { useContext, useState } from "react"; 2 | 3 | import axios from "axios"; 4 | 5 | import { PersonObject } from "react-chat-engine-advanced"; 6 | 7 | import { useIsMobile } from "../functions/isMobile"; 8 | import { Context } from "../functions/context"; 9 | import { privateKey } from "../functions/constants"; 10 | 11 | import TextInput from "./components/TextInput"; 12 | import PhotoInput from "./components/PhotoInput"; 13 | import Button from "./components/Button"; 14 | import Link from "./components/Link"; 15 | 16 | interface SignUpFormProps { 17 | onHasAccount: () => void; 18 | } 19 | 20 | const SignUpForm = (props: SignUpFormProps) => { 21 | // State 22 | const [firstName, setFirstName] = useState(""); 23 | const [lastName, setLastName] = useState(""); 24 | const [email, setEmail] = useState(""); 25 | const [password, setPassword] = useState(""); 26 | const [avatar, setAvatar] = useState(undefined); 27 | // Hooks 28 | const { setUser } = useContext(Context); 29 | const isMobile: boolean = useIsMobile(); 30 | 31 | const onSubmit = (event: React.FormEvent) => { 32 | event.preventDefault(); 33 | 34 | const userJson: PersonObject = { 35 | email: email, 36 | username: email, 37 | first_name: firstName, 38 | last_name: lastName, 39 | secret: password, 40 | avatar: null, 41 | custom_json: {}, 42 | is_online: true, 43 | }; 44 | 45 | let formData = new FormData(); 46 | formData.append("email", email); 47 | formData.append("username", email); 48 | formData.append("first_name", firstName); 49 | formData.append("last_name", lastName); 50 | formData.append("secret", password); 51 | if (avatar) { 52 | formData.append("avatar", avatar, avatar.name); 53 | } 54 | 55 | const headers = { "Private-Key": privateKey }; 56 | 57 | axios 58 | .post("https://api.chatengine.io/users/", formData, { 59 | headers, 60 | }) 61 | .then((r) => { 62 | if (r.status === 201) { 63 | userJson.avatar = r.data.avatar; 64 | setUser(userJson); 65 | } 66 | }) 67 | .catch((e) => console.log("Error", e)); 68 | }; 69 | 70 | return ( 71 | 72 | Create an account 73 | 74 | 75 | Already a member?{" "} 76 | props.onHasAccount()}>Log in 77 | 78 | 79 | 80 | setFirstName(e.target.value)} 86 | /> 87 | 88 | setLastName(e.target.value)} 97 | /> 98 | 99 | setEmail(e.target.value)} 105 | /> 106 | 107 | setPassword(e.target.value)} 117 | /> 118 | 119 | { 125 | if (e.target.files !== null) { 126 | setAvatar(e.target.files[0]); 127 | } 128 | }} 129 | /> 130 | 131 | 138 | Sign Up 139 | 140 | 141 | 142 | ); 143 | }; 144 | 145 | export default SignUpForm; 146 | -------------------------------------------------------------------------------- /src/ChatsPage/ChatHeader.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | ChatHeaderProps, 3 | ChatObject, 4 | PersonObject, 5 | Avatar, 6 | } from "react-chat-engine-advanced"; 7 | 8 | import { 9 | PhoneFilled, 10 | DeleteFilled, 11 | PaperClipOutlined, 12 | LoadingOutlined, 13 | } from "@ant-design/icons"; 14 | 15 | import axios from "axios"; 16 | 17 | import { nowTimeStamp } from "../functions/dates"; 18 | import { getOtherUser } from "../functions/getOtherUser"; 19 | import { useIsMobile } from "../functions/isMobile"; 20 | 21 | import { privateKey, projectId } from "../functions/constants"; 22 | import { useState } from "react"; 23 | 24 | interface CustomChatHeaderProps extends ChatHeaderProps { 25 | chat?: ChatObject; 26 | username: string; 27 | secret: string; 28 | } 29 | 30 | const ChatHeader = (props: CustomChatHeaderProps) => { 31 | // State 32 | const [isFilePickerLoading, setFilePickerLoading] = useState(false); 33 | const [isDeleteLoading, setDeleteLoading] = useState(false); 34 | // Hooks 35 | const isMobile: boolean = useIsMobile(); 36 | 37 | // TODO: Show how TS recommends props.chat && 38 | const otherMember: PersonObject | undefined = 39 | props.chat && getOtherUser(props.chat, props.username); 40 | 41 | const onFilesSelect: React.ChangeEventHandler = (e) => { 42 | if (!props.chat) return; 43 | setFilePickerLoading(true); 44 | 45 | const headers = { 46 | "Project-ID": projectId, 47 | "User-Name": props.username, 48 | "User-Secret": props.secret, 49 | }; 50 | 51 | const formdata = new FormData(); 52 | const filesArr = Array.from(e.target.files !== null ? e.target.files : []); 53 | filesArr.forEach((file) => formdata.append("attachments", file, file.name)); 54 | formdata.append("created", nowTimeStamp()); 55 | formdata.append("sender_username", props.username); 56 | formdata.append("custom_json", JSON.stringify({})); 57 | 58 | axios 59 | .post( 60 | `https://api.chatengine.io/chats/${props.chat.id}/messages/`, 61 | formdata, 62 | { headers } 63 | ) 64 | .then((r) => setFilePickerLoading(false)) 65 | .catch((e) => setFilePickerLoading(false)); 66 | }; 67 | 68 | const onDelete = () => { 69 | if (!props.chat) return; 70 | setDeleteLoading(true); 71 | 72 | const headers = { "Private-Key": privateKey }; 73 | axios 74 | .delete(`https://api.chatengine.io/chats/${props.chat.id}/`, { 75 | headers, 76 | }) 77 | .then(() => setDeleteLoading(false)) 78 | .catch(() => setDeleteLoading(false)); 79 | }; 80 | 81 | return ( 82 | 83 | {otherMember && ( 84 | 85 | 91 | 92 | 93 | 94 | {otherMember.first_name} {otherMember.last_name} 95 | 96 | 97 | {otherMember.is_online ? "Online" : "Offline"} 98 | 99 | 100 | 101 | 102 | 103 | 104 | {isFilePickerLoading ? ( 105 | 106 | ) : ( 107 | 108 | )} 109 | 110 | 117 | 118 | 119 | 120 | 121 | {isDeleteLoading ? ( 122 | 123 | ) : ( 124 | onDelete()} 126 | className="ce-custom-header-icon" 127 | /> 128 | )} 129 | 130 | 131 | )} 132 | 133 | 144 | 145 | ); 146 | }; 147 | 148 | export default ChatHeader; 149 | -------------------------------------------------------------------------------- /src/theme.css: -------------------------------------------------------------------------------- 1 | .ce-chat-list { background-color: rgb(40,43,54) !important; } 2 | .ce-chat-form { background-color: rgb(40,43,54) !important; padding-bottom: 14px !important; } 3 | .ce-chat-form-title { color: white !important; font-family: 'VisbyRoundCF-DemiBold' !important; } 4 | .ce-default-button { border: none !important; background-color: rgb(40,43,54) !important; color: white !important; } 5 | .ce-text-input { background-color: rgb(40,43,54) !important; color: white !important; font-family: 'VisbyRoundCF-DemiBold' !important; border: 2px solid #fa541c !important; border-radius: 8px !important; } 6 | .ce-text-input::placeholder { color: white !important; } 7 | .ce-chat-card { border: 1px solid #3e404b !important; background-color: #3e404b !important; margin: 10px 12px !important; height: 68px !important; } 8 | .ce-chat-card:hover { border: 1px solid #1890ff !important; box-shadow: rgb(24 144 255 / 35%) 0px 2px 7px !important; } 9 | .ce-chat-card-loading { height: 10px !important; } 10 | .ce-chat-card-title-loading { top: 16px !important; } 11 | .ce-active-chat-card { border: 1px solid #1890ff !important; background-color: #1890ff !important; box-shadow: rgb(24 144 255 / 35%) 0px 2px 7px !important; color: white !important; } 12 | .ce-chat-card-title { color: white !important; font-family: 'VisbyRoundCF-DemiBold' !important; } 13 | .ce-chat-card-subtitle { font-family: 'VisbyRoundCF-DemiBold' !important; font-size: 12px !important; bottom: 16px !important; width: calc(70% - 44px) !important; color: #c5c5c5 !important; } 14 | .ce-chat-card-time-stamp { font-family: 'VisbyRoundCF-DemiBold' !important; font-size: 12px !important; bottom: 16px !important; } 15 | .ce-chat-card-unread { top: calc((68px - 12px) / 2) !important; } 16 | .ce-avatar-status { border: 2px solid rgb(40,43,54) !important; width: 10px !important; height: 10px !important; } 17 | .ce-chat-card-avatar { top: 12px !important; } 18 | .ce-chat-feed-column { border: none !important; } 19 | .ce-chat-feed { background-color: rgb(40,43,54) !important; } 20 | .ce-message-list { margin-top: 24px !important; margin-left: 12px !important; margin-right: 12px !important; padding: 0px 3.3vw !important; background: linear-gradient(0deg, rgba(62,64,75,1) 0%, rgba(62,64,75,1) 75%, rgba(40,43,54,1) 100%); border-radius: 8px 8px 0px 0px !important; height: calc((100% - 85px) - 72px - 24px - 12px) !important; } 21 | .ce-message-date-text { font-family: 'VisbyRoundCF-DemiBold' !important; color: rgb(153, 153, 153) !important; font-size: 14px !important; letter-spacing: -1px; } 22 | .ce-my-message-body { font-family: 'VisbyRoundCF-Regular' !important; font-size: 12px !important; padding: 15px !important; } 23 | .ce-my-message-timestamp { font-family: 'VisbyRoundCF-DemiBold' !important; font-size: 12px !important; padding: 15px !important; margin-right: 0px !important; letter-spacing: -1px; } 24 | 25 | .ce-their-message-body { font-family: 'VisbyRoundCF-Regular' !important; font-size: 12px !important; padding: 15px !important; background-color: #434756 !important; color: white !important; } 26 | .ce-their-message-timestamp { font-family: 'VisbyRoundCF-DemiBold' !important; font-size: 12px !important; padding: 15px !important; margin-left: 0px !important; letter-spacing: -1px; } 27 | 28 | .ce-their-message-timestamp { color: rgb(241, 240, 240) !important; letter-spacing: -1px; } 29 | .ce-their-message-sender-username { color: #999 !important; } 30 | .ce-message-file { background-color: #434758 !important; color: #c5c5c5 !important; border-radius: 8px !important; } 31 | .ce-message-image { background-color: #434758 !important; color: #c5c5c5 !important; border-radius: 8px !important; padding: 0px !important; max-width: 124px !important; max-height: 124px !important; } 32 | 33 | .ce-mobile-chat-list-button { top: 32px !important; left: 0px !important; } 34 | .ce-mobile-chat-settings-button { display: none !important; } 35 | 36 | .ce-custom-chat-header { display: inline-block; position: relative; width: 100%; height: 86px; } 37 | .ce-custom-header-text { display: inline-block; max-width: 50%; padding-left: 14px; position: relative; top: 21px; } 38 | .ce-custom-header-title { color: white; font-size: 13px; font-family: 'VisbyRoundCF-DemiBold'; } 39 | .ce-custom-header-subtitle { color: rgb(153, 153, 153); font-size: 11px; } 40 | 41 | .ce-custom-header-icon-wrapper { display: inline-block; max-width: 50%; position: relative; top: 36px; float: right; } 42 | .ce-custom-header-icon { margin-right: 12px; cursor: pointer; color: rgb(153, 153, 153) !important; transition: all 0.66s ease; } 43 | .ce-custom-header-icon:hover { color: rgb(24, 144, 255) !important; } 44 | 45 | .ce-custom-message-form { position: relative; height: 68px; margin-left: 12px; margin-right: 12px; width: calc(100% - 12px - 12px); border-radius: 0px 0px 8px 8px; background-color: #3e404b; } 46 | .ce-custom-message-input { position: absolute; top: 12px; left: 3.3vw; width: calc(100% - 3.3vw - 3.3vw - 14px - 15px - 15px); box-shadow: rgba(24, 144, 255, 0.35) 0px 2px 7px; border: 1px solid rgb(24, 144, 255); outline: none; background-color: #434756; color: white; font-size: 12px; padding: 0px 15px; font-family: 'VisbyRoundCF-DemiBold'; height: 36px; border-radius: 8px; transition: all .44s ease; } 47 | .ce-custom-message-input:focus { box-shadow: rgba(64, 169, 255, 0.35) 0px 2px 7px; border: 1px solid #40a9ff; } 48 | .ce-custom-message-input::placeholder { color: #e1e1e1; } 49 | .ce-custom-send-button { cursor: pointer; background-color: rgb(24, 144, 255); border: 1px solid rgb(24, 144, 255); width: 36px; height: 36px; border-radius: 8px; color: white; box-shadow: rgba(24, 144, 255, 0.35) 0px 5px 15px; position: absolute; top: 12px; right: 3.3vw; transition: all .44s ease; } 50 | .ce-custom-send-button:hover { background-color: #40a9ff; } 51 | 52 | .ce-sidebar-menu { position: absolute; top: 30vh; } 53 | .ce-sidebar-icon { width: 6vw; padding-top: 12px; padding-bottom: 12px; font-size: 16px; color: rgb(153, 153, 153) !important; } 54 | .ce-sidebar-icon-active { color: rgb(24, 144, 255) !important; border-left: 2px solid rgb(24, 144, 255); } 55 | .sidebar-avatar { position: absolute !important; bottom: 66px; left: calc(50% - 22px); border: 1px solid rgb(24, 144, 255); box-shadow: rgb(24 144 255 / 35%) 0px 2px 7px; } 56 | .signout-icon { cursor: pointer; color: rgb(153, 153, 153) !important; transition: all 0.66s ease; font-size: 18px; position: absolute; bottom: 24px; left: calc(50% - 9px); } 57 | .signout-icon:hover { color: #1890ff !important; } 58 | 59 | .ce-chat-form-autocomplete { width: calc(100% - 12px - 12px) !important; margin: 0px 12px !important; padding-top: 28px !important; padding-bottom: 32px !important; } 60 | .ant-input-lg { background-color: rgb(40,43,54) !important; outline: none !important; border: 1px solid rgb(40,43,54) !important; color: white !important; border-radius: 8px 0px 0px 8px !important; } 61 | .ant-input-lg::placeholder { color: white !important; font-family: 'VisbyRoundCF-DemiBold' !important; padding-top: 12px !important; } 62 | .ant-input-search-button { background-color: rgb(40,43,54) !important; border: none !important; outline: none !important; margin-left: 3px !important; border-radius: 0px 8px 8px 0px !important; } 63 | .ant-input-search-button:hover { background-color: rgb(40,43,54) !important; } 64 | .ant-input-group-addon { background-color: rgb(40,43,54) !important; } 65 | 66 | .ce-empty-settings { background-color: #282b36 !important; width: 3vw; height: 100vh; } --------------------------------------------------------------------------------