├── src ├── react-app-env.d.ts ├── setupTests.ts ├── index.css ├── service │ └── supabase.ts ├── reportWebVitals.ts ├── utils.ts ├── index.tsx ├── hooks │ ├── useAuth.ts │ └── useMessage.ts ├── App.tsx └── components │ ├── LoginModal.tsx │ ├── SignUpModal.tsx │ ├── Header.tsx │ └── MessageList.tsx ├── public ├── logo.png ├── robots.txt ├── favicon.ico ├── manifest.json └── index.html ├── .env.example ├── .gitignore ├── tsconfig.json ├── README.md └── package.json /src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /public/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samarthmn/supabase-chat-app/HEAD/public/logo.png -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samarthmn/supabase-chat-app/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | PUBLIC_URL="/" 2 | REACT_APP_SUPABASE_URL="" 3 | REACT_APP_SUPABASE_PUBLIC_KEY="" -------------------------------------------------------------------------------- /src/setupTests.ts: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom'; 6 | -------------------------------------------------------------------------------- /.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 | 25 | .env 26 | # Local Netlify folder 27 | .netlify -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | @import "~antd/dist/antd.css"; 2 | 3 | body { 4 | margin: 0; 5 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", 6 | "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", 7 | sans-serif; 8 | -webkit-font-smoothing: antialiased; 9 | -moz-osx-font-smoothing: grayscale; 10 | } 11 | 12 | code { 13 | font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New", 14 | monospace; 15 | } 16 | -------------------------------------------------------------------------------- /src/service/supabase.ts: -------------------------------------------------------------------------------- 1 | import { createClient, SupabaseClientOptions } from "@supabase/supabase-js"; 2 | 3 | const supabaseOptions: SupabaseClientOptions = { 4 | autoRefreshToken: true, 5 | persistSession: true, 6 | detectSessionInUrl: true, 7 | }; 8 | 9 | const supabaseClient = createClient( 10 | process.env.REACT_APP_SUPABASE_URL || "", 11 | process.env.REACT_APP_SUPABASE_PUBLIC_KEY || "", 12 | supabaseOptions 13 | ); 14 | 15 | export { supabaseClient }; 16 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "SupaBase Chat App", 3 | "name": "Real Time Chat App", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "48x48", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo.png", 12 | "type": "image/png", 13 | "sizes": "512x512" 14 | } 15 | ], 16 | "start_url": ".", 17 | "display": "standalone", 18 | "theme_color": "#000000", 19 | "background_color": "#ffffff" 20 | } 21 | -------------------------------------------------------------------------------- /src/reportWebVitals.ts: -------------------------------------------------------------------------------- 1 | import { ReportHandler } from 'web-vitals'; 2 | 3 | const reportWebVitals = (onPerfEntry?: ReportHandler) => { 4 | if (onPerfEntry && onPerfEntry instanceof Function) { 5 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { 6 | getCLS(onPerfEntry); 7 | getFID(onPerfEntry); 8 | getFCP(onPerfEntry); 9 | getLCP(onPerfEntry); 10 | getTTFB(onPerfEntry); 11 | }); 12 | } 13 | }; 14 | 15 | export default reportWebVitals; 16 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | export const hideEmail = function (email: string) { 2 | if (email.split("@")[0].length <= 2) { 3 | return email.replace(/(.{0})(.*)(?=@)/, function (gp1, gp2, gp3) { 4 | for (let i = 0; i < gp3.length; i++) { 5 | gp2 += "*"; 6 | } 7 | return gp2; 8 | }); 9 | } 10 | return email.replace(/(.{2})(.*)(?=@)/, function (gp1, gp2, gp3) { 11 | for (let i = 0; i < gp3.length; i++) { 12 | gp2 += "*"; 13 | } 14 | return gp2; 15 | }); 16 | }; 17 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | import App from "./App"; 4 | import reportWebVitals from "./reportWebVitals"; 5 | import "./index.css"; 6 | 7 | ReactDOM.render( 8 | 9 | 10 | , 11 | document.getElementById("root") 12 | ); 13 | 14 | // If you want to start measuring performance in your app, pass a function 15 | // to log results (for example: reportWebVitals(console.log)) 16 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals 17 | reportWebVitals(); 18 | -------------------------------------------------------------------------------- /src/hooks/useAuth.ts: -------------------------------------------------------------------------------- 1 | import { supabaseClient } from "../service/supabase"; 2 | 3 | const useAuth = () => { 4 | const signIn = (email: string, password: string) => 5 | supabaseClient.auth.signIn({ email, password }); 6 | 7 | const signUp = (email: string, password: string) => 8 | supabaseClient.auth.signUp({ email, password }); 9 | 10 | const getSession = async () => { 11 | const session = supabaseClient.auth.session(); 12 | return session; 13 | }; 14 | return { 15 | signUp, 16 | signIn, 17 | getSession, 18 | }; 19 | }; 20 | 21 | export default useAuth; 22 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [](https://app.netlify.com/sites/supabase-chat-app/deploys) 2 | 3 | # Real time chat 4 | 5 | Real-time chat app using SupaBase as back-end and ReactJS as front-end. 6 | 7 | ### Installation 8 | 9 | #### Clone the repo and install the dependence 10 | 11 | ```sh 12 | git clone https://github.com/SamarthMN/supabase-chat-app 13 | cd supabase-chat-app && npm install 14 | npm run start 15 | ``` 16 | 17 | #### Copy .env from .env.example file 18 | 19 | ``` 20 | cp .env.example .env 21 | ``` 22 | 23 | ##### Update `REACT_APP_SUPABASE_URL` and `REACT_APP_SUPABASE_PUBLIC_KEY` in the `.env` file 24 | 25 | ### Run the project 26 | 27 | In the project directory, you can run: 28 | 29 | ##### `npm start` or `npm dev` 30 | 31 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser. 32 | 33 | ##### `npm run build` 34 | 35 | Builds the app for production to the `build` folder. 36 | -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | import { Layout } from "antd"; 3 | import { supabaseClient } from "./service/supabase"; 4 | import { User } from "@supabase/supabase-js"; 5 | import useAuth from "./hooks/useAuth"; 6 | import Header from "./components/Header"; 7 | import MessageList from "./components/MessageList"; 8 | 9 | const { Content } = Layout; 10 | 11 | const App = () => { 12 | const [user, setUser] = useState(null); 13 | const { getSession } = useAuth(); 14 | 15 | useEffect(() => { 16 | supabaseClient.auth.onAuthStateChange((auth, session) => { 17 | switch (auth) { 18 | case "SIGNED_IN": 19 | session?.user && setUser(session.user); 20 | break; 21 | case "SIGNED_OUT": 22 | setUser(null); 23 | break; 24 | case "USER_UPDATED": 25 | session?.user && setUser(session.user); 26 | break; 27 | } 28 | }); 29 | }, []); 30 | useEffect(() => { 31 | getSession().then((session) => { 32 | if (session) { 33 | setUser(session.user); 34 | } else { 35 | setUser(null); 36 | } 37 | }); 38 | // eslint-disable-next-line 39 | }, []); 40 | return ( 41 | 42 | 43 | 44 | 45 | 46 | 47 | ); 48 | }; 49 | 50 | export default App; 51 | -------------------------------------------------------------------------------- /src/hooks/useMessage.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | import { supabaseClient } from "../service/supabase"; 3 | import { v4 as uuidv4 } from "uuid"; 4 | 5 | export type MESSAGE = { 6 | id: string; 7 | message: string; 8 | user_id: string; 9 | created_at: string; 10 | }; 11 | 12 | const useMessage = () => { 13 | const [messages, setMessages] = useState([]); 14 | const [limit, setLimit] = useState(5); 15 | const [loading, setLoading] = useState(false); 16 | 17 | useEffect(() => { 18 | supabaseClient.from("messages").on("INSERT", handleInsert).subscribe(); 19 | // eslint-disable-next-line react-hooks/exhaustive-deps 20 | }, [messages]); 21 | 22 | useEffect(() => { 23 | setLoading(true); 24 | supabaseClient 25 | .from("messages") 26 | .select() 27 | .order("created_at", { ascending: false }) 28 | .limit(limit) 29 | .then((data) => { 30 | setLoading(false); 31 | if (!data.error && data.data) { 32 | data.data.reverse(); 33 | setMessages(data.data); 34 | } 35 | }); 36 | }, [limit]); 37 | 38 | const handleInsert = (payload: { new: MESSAGE }) => { 39 | setMessages([...messages, payload.new]); 40 | }; 41 | 42 | const addNewMessage = async (user_id: string, message: string) => { 43 | const { data, error } = await supabaseClient 44 | .from("messages") 45 | .insert([{ id: uuidv4(), user_id, message }]); 46 | return { data, error }; 47 | }; 48 | 49 | const increaseLimit = () => { 50 | setLimit(limit + 5); 51 | }; 52 | 53 | return { 54 | loading, 55 | messages, 56 | addNewMessage, 57 | increaseLimit, 58 | }; 59 | }; 60 | 61 | export default useMessage; 62 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 16 | 17 | 26 | React App 27 | 28 | 29 | You need to enable JavaScript to run this app. 30 | 31 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "supabase-chat-app", 3 | "version": "1.0.0", 4 | "description": "Real-time chat app using SupaBase as back-end and ReactJS as front-end", 5 | "scripts": { 6 | "start": "react-scripts start", 7 | "dev": "npm run start", 8 | "build": "react-scripts build", 9 | "deploy": "npm run build && netlify deploy --prod" 10 | }, 11 | "dependencies": { 12 | "@ant-design/icons": "^4.3.0", 13 | "@supabase/supabase-js": "^1.1.2", 14 | "@testing-library/jest-dom": "^5.11.4", 15 | "@testing-library/react": "^11.1.0", 16 | "@testing-library/user-event": "^12.1.10", 17 | "antd": "^4.9.4", 18 | "moment": "^2.29.1", 19 | "react": "^17.0.2", 20 | "react-dom": "^17.0.2", 21 | "react-scripts": "4.0.3", 22 | "typescript": "^4.1.2", 23 | "uuid": "^8.3.2", 24 | "web-vitals": "^1.0.1" 25 | }, 26 | "devDependencies": { 27 | "@types/jest": "^26.0.15", 28 | "@types/node": "^12.0.0", 29 | "@types/react": "^17.0.0", 30 | "@types/react-dom": "^17.0.0", 31 | "@types/react-router-dom": "^5.1.6", 32 | "@types/uuid": "^8.3.0" 33 | }, 34 | "repository": { 35 | "type": "git", 36 | "url": "git+https://github.com/SamarthMN/supabase-chat-app.git" 37 | }, 38 | "keywords": [ 39 | "supabase", 40 | "chat-app", 41 | "reactjs", 42 | "antd", 43 | "ant-design", 44 | "TypeScript" 45 | ], 46 | "author": "Samarth MN ", 47 | "license": "ISC", 48 | "bugs": { 49 | "url": "https://github.com/SamarthMN/supabase-chat-app/issues" 50 | }, 51 | "homepage": "https://github.com/SamarthMN/supabase-chat-app#readme", 52 | "eslintConfig": { 53 | "extends": [ 54 | "react-app", 55 | "react-app/jest" 56 | ] 57 | }, 58 | "browserslist": { 59 | "production": [ 60 | ">0.2%", 61 | "not dead", 62 | "not op_mini all" 63 | ], 64 | "development": [ 65 | "last 1 chrome version", 66 | "last 1 firefox version", 67 | "last 1 safari version" 68 | ] 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/components/LoginModal.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | import { Alert, Button, Form, Input, Modal } from "antd"; 3 | import { supabaseClient } from "../service/supabase"; 4 | type ON_SUBMIT = { 5 | email: string; 6 | password: string; 7 | }; 8 | 9 | type LOGIN_MODAL = { 10 | isModalOpen: boolean; 11 | onClose: () => void; 12 | }; 13 | 14 | const LoginModal = ({ isModalOpen = false, onClose }: LOGIN_MODAL) => { 15 | const [error, setError] = useState(""); 16 | const [loading, setLoading] = useState(false); 17 | const onFinish = ({ email, password }: ON_SUBMIT) => { 18 | supabaseClient.auth 19 | .signIn({ email, password }) 20 | .then((data) => { 21 | setLoading(false); 22 | if (!data.error && data.user) { 23 | onClose(); 24 | } else if (data.error) { 25 | setError(data.error.message); 26 | } 27 | }) 28 | .catch((err) => { 29 | setError(err); 30 | setLoading(false); 31 | }); 32 | }; 33 | return ( 34 | 42 | 48 | 53 | setError("")} /> 54 | 55 | 56 | 61 | setError("")} /> 62 | 63 | 64 | 65 | Login 66 | 67 | 68 | {error && } 69 | 70 | 71 | ); 72 | }; 73 | 74 | export default LoginModal; 75 | -------------------------------------------------------------------------------- /src/components/SignUpModal.tsx: -------------------------------------------------------------------------------- 1 | import { Alert, Button, Form, Input, Modal } from "antd"; 2 | import { useState } from "react"; 3 | import { supabaseClient } from "../service/supabase"; 4 | type ON_SUBMIT = { 5 | email: string; 6 | password: string; 7 | }; 8 | 9 | type SIGNUP_MODAL = { 10 | isModalOpen: boolean; 11 | onClose: () => void; 12 | }; 13 | 14 | const SignUpModal = ({ isModalOpen = false, onClose }: SIGNUP_MODAL) => { 15 | const [error, setError] = useState(""); 16 | const [loading, setLoading] = useState(false); 17 | const onFinish = ({ email, password }: ON_SUBMIT) => { 18 | setLoading(true); 19 | supabaseClient.auth 20 | .signUp({ email, password }) 21 | .then((data) => { 22 | setLoading(false); 23 | if (!data.error && data.user) { 24 | onClose(); 25 | } else if (data.error) { 26 | setError(data.error.message); 27 | } 28 | }) 29 | .catch((err) => { 30 | setError(err); 31 | setLoading(false); 32 | }); 33 | }; 34 | 35 | return ( 36 | 44 | 50 | 55 | setError("")} /> 56 | 57 | 58 | 63 | setError("")} /> 64 | 65 | 66 | 67 | Login 68 | 69 | 70 | {error && } 71 | 72 | 73 | ); 74 | }; 75 | 76 | export default SignUpModal; 77 | -------------------------------------------------------------------------------- /src/components/Header.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | import { Layout } from "antd"; 3 | import { GithubOutlined } from "@ant-design/icons"; 4 | import { User } from "@supabase/supabase-js"; 5 | 6 | import LoginModal from "./LoginModal"; 7 | import SignUpModal from "./SignUpModal"; 8 | import { supabaseClient } from "../service/supabase"; 9 | const { Header: AntHeader } = Layout; 10 | 11 | const Header = ({ user }: { user: User | null }) => { 12 | const [loginModal, setLoginModal] = useState(false); 13 | const [signUpModal, setSignUpModal] = useState(false); 14 | const onLogout = () => supabaseClient.auth.signOut(); 15 | return ( 16 | 17 | setLoginModal(false)} 20 | /> 21 | setSignUpModal(false)} 24 | /> 25 | 26 | 33 | 34 | Real Time Chat App 35 | 38 | window.open( 39 | "https://github.com/SamarthMN/supabase-chat-app", 40 | "_blank" 41 | ) 42 | } 43 | /> 44 | 45 | 46 | {!user ? ( 47 | <> 48 | setLoginModal(true)} 55 | > 56 | Login 57 | 58 | 65 | or 66 | 67 | setSignUpModal(true)} 74 | > 75 | Sign Up 76 | 77 | > 78 | ) : ( 79 | 82 | ({user.email}) -{" "} 83 | 84 | Logout 85 | 86 | 87 | )} 88 | 89 | 90 | 91 | ); 92 | }; 93 | 94 | export default Header; 95 | -------------------------------------------------------------------------------- /src/components/MessageList.tsx: -------------------------------------------------------------------------------- 1 | import { useRef } from "react"; 2 | import { User } from "@supabase/supabase-js"; 3 | import { 4 | message as messageAlert, 5 | Col, 6 | List, 7 | Row, 8 | Form, 9 | Input, 10 | Button, 11 | Comment, 12 | Tooltip, 13 | FormInstance, 14 | } from "antd"; 15 | import moment from "moment"; 16 | import useMessage, { MESSAGE } from "../hooks/useMessage"; 17 | 18 | const MessageList = ({ user }: { user: User | null }) => { 19 | const { messages, addNewMessage, increaseLimit, loading } = useMessage(); 20 | const formRef = useRef(null); 21 | const listRef = useRef(null); 22 | const onNewMessage = async ({ message }: MESSAGE) => { 23 | if (user?.id) { 24 | formRef.current?.resetFields(); 25 | await addNewMessage(user.id, message); 26 | listRef.current?.scrollTo({ 27 | top: document.documentElement.scrollHeight, 28 | behavior: "auto", 29 | }); 30 | } else if (message) { 31 | messageAlert.error("Please Login / Sign Up", 2.5); 32 | } else { 33 | messageAlert.error("Enter Message", 2.5); 34 | } 35 | }; 36 | return ( 37 | <> 38 | 39 | 40 | 41 | Load More 42 | 43 | 44 | 49 | ( 58 | 68 | {moment(item.created_at).fromNow()} 69 | 70 | } 71 | /> 72 | )} 73 | /> 74 | 75 | 76 | 77 | 78 | 85 | 86 | 87 | 88 | 89 | 90 | Submit 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | > 99 | ); 100 | }; 101 | 102 | export default MessageList; 103 | --------------------------------------------------------------------------------