import React from 'react';
import ReactDOM from 'react-dom/client';
import { initializeApp } from "firebase/app";
import { BrowserRouter } from 'react-router-dom';
import 'bootstrap/dist/css/bootstrap.css';
import './index.css';
import App from './components/App';
// Your web app's Firebase configuration
const firebaseConfig = {
apiKey: "AIzaSyDb5rO7-hBq0PJ3emVEwv1Uwz8K3Dtz7kk",
authDomain: "react-chat-wi23a.firebaseapp.com",
projectId: "react-chat-wi23a",
storageBucket: "react-chat-wi23a.appspot.com",
messagingSenderId: "716444400015",
appId: "1:716444400015:web:a3c6f14042da5bf100f3fa"
};
// Initialize Firebase
const app = initializeApp(firebaseConfig);
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<BrowserRouter>
<App />
</BrowserRouter>
);
iimport React, { useState, useEffect } from 'react';
import { Routes, Route, Outlet, Navigate, useNavigate } from 'react-router-dom';
import { HeaderBar } from './HeaderBar.js';
import ChatPage from './ChatPage';
import SignInPage from './SignInPage';
import ProfilePage from './ProfilePage.js';
import * as Static from './StaticPages';
import DEFAULT_USERS from '../data/users.json';
export default function App(props) {
//state
const [currentUser, setCurrentUser] = useState(DEFAULT_USERS[0]) //default to null user
const navigateTo = useNavigate(); //navigation hook
//effect to run when the component first loads
useEffect(() => {
//log in a default user
// loginUser(DEFAULT_USERS[1])
}, []) //array is list of variables that will cause this to rerun if changed
const loginUser = (userObj) => {
console.log("logging in as", userObj.userName);
setCurrentUser(userObj);
if(userObj.userId !== null){
navigateTo('/chat/general'); //go to chat after login
}
}
//... more App on next slide
No longer initially logging in a user in App
Reminder this will be passed as a prop to the Sign-in Component
//... App continued
return (
<div className="container-fluid d-flex flex-column">
<HeaderBar currentUser={currentUser} />
<Routes>
<Route index element={<Static.WelcomePage />} />
<Route path="about" element={<Static.AboutPage />} />
<Route path="*" element={<Static.ErrorPage />} />
<Route path="signin" element={<SignInPage currentUser={currentUser} loginCallback={loginUser}/>} />
{/* protected routes */}
<Route element={<ProtectedPage currentUser={currentUser} />}>
<Route path="chat/:channelName" element={<ChatPage currentUser={currentUser} />} />
{/* redirect to general channel */}
<Route path="chat" element={<Navigate to="/chat/general" />} />
<Route path="profile" element={<ProfilePage currentUser={currentUser} />}/>
</Route>
</Routes>
</div>
);
}
function ProtectedPage(props) {
if(props.currentUser.userId === null) { //if no user, send to sign in
return <Navigate to="/signin" />
}
else { //otherwise, show the child route content
return <Outlet />
}
}
import React from 'react';
import { Link, NavLink } from 'react-router-dom';
export function HeaderBar(props) {
const currentUser = props.currentUser;
const handleSignOut = (event) => {
console.log("signing out");
}
return (
<header className="text-light bg-primary px-1 d-flex justify-content-between">
<h1>React Chat</h1>
<ul className="nav nav-pills">
<li className="nav-item">
<NavLink className="nav-link" to="/">Home</NavLink>
</li>
<li className="nav-item">
<NavLink className="nav-link" to="/chat">Chat</NavLink>
</li>
<li className="nav-item">
<NavLink className="nav-link" to="/about">About</NavLink>
</li>
{currentUser.userId &&
<>
<li className="nav-item">
<NavLink to="/profile" className="nav-link">Profile</NavLink>
</li>
<li className="nav-item">
<button className="btn btn-secondary ms-2" onClick={handleSignOut}>Sign Out</button>
</li>
</>
}
{!currentUser.userId &&
<li className="nav-item">
<NavLink className="nav-link" to="/signin">
<img src={currentUser.userImg} alt={currentUser.userName + " avatar"} />
</NavLink>
</li>
}
</ul>
</header>
)
}
If we have user, show the Profile Component
If no user, show Sign-in Component
Callback for the onClick on the SignOut Button
export default function ChatPage(props) {
const [chatMessages, setChatMessages] = useState([]);
...
useEffect(() => {
const db = getDatabase(); //"the database"
const allMessageRef = ref(db, "allMessages");
const offFunction = onValue(allMessageRef, (snapshot) => { //when db value changes
const valueObj = snapshot.val();
const objKeys = Object.keys(valueObj); //convert object into array
const objArray = objKeys.map((keyString) => {
const theMessageObj = valueObj[keyString];
theMessageObj.key = keyString;
return theMessageObj;
})
setChatMessages(objArray);
})
function cleanup() {
console.log("component is being removed");
offFunction(); //when the component goes away, we turn off the listener
}
return cleanup; //return instructions on how to turn off lights
}, [])
const addMessage = (messageText) => {
const userObj = currentUser;
const newMessage = {
"userId": userObj.userId,
"userName": userObj.userName,
"userImg": userObj.userImg,
"text": messageText,
"timestamp": Date.now(),
"channel": currentChannel
}
const db = getDatabase(); //"the database"
const allMessageRef = ref(db, 'allMessages');
firebasePush(allMessageRef, newMessage);
}
//... More ChatPage below, but won't show on the next slide
The addMessage callback will get the reference to the allMessages key, and then push the new message to the database on Firebase
Use Effect hook to load the messages from firebase when component loads 1st time
Register 'onValue' listener for any change to the Messages on Firebase
If there is a change we get the new messages array and set state to cause re-render
import React, { useState } from 'react';
export default function ProfilePage(props) {
//convenience
const displayName = props.currentUser.userName;
const [imageFile, setImageFile] = useState(undefined)
let initialURL = props.currentUser.userImg;
const [imageUrl, setImageUrl] = useState(initialURL)
const handleChange = (event) => { //image uploading!
if(event.target.files.length > 0 && event.target.files[0]) {
const imageFile = event.target.files[0]
setImageFile(imageFile);
setImageUrl(URL.createObjectURL(imageFile));
}
}
const handleImageUpload = (event) => {
console.log("Uploading", imageFile);
}
return (
<div className="container">
<h2>
{props.currentUser.userName && displayName+"'s"} Profile
</h2>
<div className="mb-5 image-upload-form">
<img src={imageUrl} alt="user avatar preview" className="mb-2"/>
<label htmlFor="imageUploadInput" className="btn btn-sm btn-secondary me-2">Choose Image</label>
<button className="btn btn-sm btn-success" onClick={handleImageUpload}>Save to Profile</button>
<input type="file" name="image" id="imageUploadInput" className="d-none" onChange={handleChange}/>
</div>
</div>
)
}
Set Up Firebase if you haven't already
Include Firebase in your React app
Include FirebaseUI in your React app
A library (provided by Firebase) that created a sign-in form for your page.
The React bindings for FirebaseUI (firebaseui-web-react) has not been updated for React 18 yet, and appears to have been abandoned. See the open pull request from Gabriel Villenave.
//in package.json
"react-firebaseui": "https://gitpkg.now.sh/gvillenave/firebaseui-web-react/dist"
# Install library (on command line)
npm install https://gitpkg.now.sh/gvillenave/firebaseui-web-react/dist
A second option is to instead install the firebaseui library (instead of the React bindings) and copy in the StyledFirebaseAuth.tsx file yourself.
Until the pull request is accepted, one work around is to install the updated fork instead of the usual package:
The FirebaseUI library provides pre-defined UI elements including a "login form" that supports the different authentication methods for the different providers
The <StyledFirebaseAuth> Component can be rendered like any other Component. It requires 2 props:
We will install the fork instead
import React from 'react';
import { getAuth, EmailAuthProvider, GoogleAuthProvider } from 'firebase/auth';
import { StyledFirebaseAuth } from 'react-firebaseui';
...
export default function SignInPage(props) {
const currentUser = props.currentUser;
const loginFunction = props.loginCallback;
const auth = getAuth(); //the authenticator
const configObj = {
signInOptions: [
{
provider: EmailAuthProvider.PROVIDER_ID,
requireDisplayName: true,
},
{
provider: GoogleAuthProvider.PROVIDER_ID
}
],
signInFlow: 'popup',
callbacks: {
signInSuccessWithAuthResult: () => false //don't do anything special on signin
},
credentialHelper: 'none'
}
...
return (
<div className="card bg-light">
<div className="container card-body">
<StyledFirebaseAuth firebaseAuth={auth} uiConfig={configObj} />
{/* <p className="lead">Pick a user:</p>
<Dropdown>
...
</Dropdown> */}
</div>
</div>
)
}
Add firebase config script config and initialize
import
Render component with 2 props
import
get the authenticator
Make it easy to test authentication by having consistent simple usernames and passwords
It is recommended to register an event listener to determine which user is logged in This event occurs when the page first loads and Firebase determines that a user has previously signed up (the “initial state” is set), or when a user logs out. You can register this listener by using the onAuthStateChanged method:
The firebase.auth() variable tracks which User is currently logged in. This info persists even after the browser is closed. Every time page is reloaded, the firebase.auth() function will perform the authentication and “re-login” the user
import React, { useState, useEffect } from 'react';
import { Routes, Route, Outlet, Navigate, useNavigate } from 'react-router-dom';
import { getAuth, onAuthStateChanged } from 'firebase/auth';
...
import DEFAULT_USERS from '../data/users.json';
export default function App(props) {
//state
const [currentUser, setCurrentUser] = useState(DEFAULT_USERS[0]) //default to null user
const navigateTo = useNavigate(); //navigation hook
//effect to run when the component first loads
useEffect(() => {
//log in a default user
// loginUser(DEFAULT_USERS[1])
const auth = getAuth();
// authenticator, a callback
onAuthStateChanged(auth, (firebaseUser) => {
if(firebaseUser) {
console.log("signing in as", firebaseUser.displayName)
console.log(firebaseUser);
firebaseUser.userId = firebaseUser.uid;
firebaseUser.userName = firebaseUser.displayName;
firebaseUser.userImg = firebaseUser.photoURL || "/img/null.png";
setCurrentUser(firebaseUser);
}
else { //no user
console.log("signed out");
setCurrentUser(DEFAULT_USERS[0]); //change the null user
}
})
}, []) //array is list of variables that will cause this to rerun if changed
...
In effect hook (runs 1st time component renders
import getAuth and onAuthStateChanged
getAuth object
Register onAuthStateChanged Listener
If there's a firebaseUser, add some additional fields for user object
Then setState for our currentUser state var
If there is no firebase user, we're signed out
import { getAuth, signOut } from 'firebase/auth';
const handleSignOut = (event) => {
//sign out here
signOut(getAuth());
}
//...
<li className="nav-item">
<button className="btn btn-secondary ms-2" onClick={handleSignOut}>Sign Out</button>
</li>
...
//little hacky
if(currentUser.userId) { //if signed in
return<Navigate to="/chat/general"/>
}
return (
<div className="card bg-light">
<div className="container card-body">
<StyledFirebaseAuth uiConfig={configObj} firebaseAuth={auth} />
</div>
</div>
)
}
import { getAuth, signOut } from 'firebase/auth';
const handleSignOut = (event) => {
//sign out here
signOut(getAuth());
}
//...
<li className="nav-item">
<button className="btn btn-secondary ms-2" onClick={handleSignOut}>Sign Out</button>
</li>
import React, { useState } from 'react';
export default function ProfilePage(props) {
const displayName = props.currentUser.userName;
const [imageFile, setImageFile] = useState(undefined)
let initialURL = props.currentUser.userImg;
const [imageUrl, setImageUrl] = useState(initialURL)
const handleChange = (event) => { //image uploading!
if(event.target.files.length > 0 && event.target.files[0]) {
const imageFile = event.target.files[0]
setImageFile(imageFile);
setImageUrl(URL.createObjectURL(imageFile));
}
}
const handleImageUpload = (event) => {
console.log("Uploading", imageFile);
}
return (
<div className="container">
<h2>
{props.currentUser.userName && displayName+"'s"} Profile
</h2>
<div className="mb-5 image-upload-form">
<img src={imageUrl} alt="user avatar preview" className="mb-2"/>
<label htmlFor="imageUploadInput" className="btn btn-sm btn-secondary me-2">Choose Image</label>
<button className="btn btn-sm btn-success" onClick={handleImageUpload}>Save to Profile</button>
<input type="file" name="image" id="imageUploadInput" className="d-none" onChange={handleChange}/>
</div>
</div>
)
}
Two state variables
on the file change below, we grab the file and setState on the file, and then create a url, and setState from the file as well
This is just a javascript function to create a url from a file
note display d-none
the label (button) looks better than the file input
here we can handle upload
import React, { useState } from 'react';
export default function ProfilePage(props) {
const displayName = props.currentUser.userName;
const [imageFile, setImageFile] = useState(undefined)
let initialURL = props.currentUser.userImg;
const [imageUrl, setImageUrl] = useState(initialURL)
const handleChange = (event) => { //image uploading!
if(event.target.files.length > 0 && event.target.files[0]) {
const imageFile = event.target.files[0]
setImageFile(imageFile);
setImageUrl(URL.createObjectURL(imageFile));
}
}
const handleImageUpload = (event) => {
console.log("Uploading", imageFile);
}
return (
<div className="container">
<h2>
{props.currentUser.userName && displayName+"'s"} Profile
</h2>
<div className="mb-5 image-upload-form">
<img src={imageUrl} alt="user avatar preview" className="mb-2"/>
<label htmlFor="imageUploadInput" className="btn btn-sm btn-secondary me-2">Choose Image</label>
<button className="btn btn-sm btn-success" onClick={handleImageUpload}>Save to Profile</button>
<input type="file" name="image" id="imageUploadInput" className="d-none" onChange={handleChange}/>
</div>
</div>
)
}
label htmlFor is a lable for the file input
file input is hidden
The label styled as button, and since its set with htmlFor to the file input it behaves as if clicking on the "choose file"
import React, { useState } from 'react';
import { getStorage, ref, uploadBytes, getDownloadURL} from 'firebase/storage';
import { updateProfile} from 'firebase/auth';
import { getDatabase, ref as dbRef, set as firebaseSet } from 'firebase/database';
export default function ProfilePage(props) {
const currentUser = props.currentUser;
const displayName = props.currentUser.userName;
const [imageFile, setImageFile] = useState(undefined)
let initialURL = props.currentUser.userImg;
const [imageUrl, setImageUrl] = useState(initialURL)
const handleChange = (event) => {
if(event.target.files.length > 0 && event.target.files[0]) {
const imageFile = event.target.files[0]
setImageFile(imageFile);
setImageUrl(URL.createObjectURL(imageFile));
}
}
...//more on next slide
imports to upload storage, update profile, get access to db
...//profile bottom half of file
const handleImageUpload = async (event) => {
console.log("Uploading", imageFile);
const storage = getStorage();
const imageRef = ref(storage, "userImages/"+currentUser.userId+".png");
await uploadBytes(imageRef, imageFile)
const downloadUrlString = await getDownloadURL(imageRef);
console.log(downloadUrlString);
await updateProfile(currentUser, { photoURL: downloadUrlString} ); //put in user profile
const db = getDatabase(); //also put in database (for fun)
const refString = "userData/"+currentUser.userId+"/imgUrl";
console.log(refString);
const userImgRef = dbRef(db, "userData/"+currentUser.userId+"/imgUrl")
firebaseSet(userImgRef, downloadUrlString);
}
return (
<div className="container">
<h2>
{props.currentUser.userName && displayName+"'s"} Profile
</h2>
<div className="mb-5 image-upload-form">
<img src={imageUrl} alt="user avatar preview" className="mb-2"/>
<label htmlFor="imageUploadInput" className="btn btn-sm btn-secondary me-2">Choose Image</label>
<button className="btn btn-sm btn-success" onClick={handleImageUpload}>Save to Profile</button>
<input type="file" name="image" id="imageUploadInput" className="d-none" onChange={handleChange}/>
</div>
</div>
)
}
asynchronous
getStorage db
get ref to storage location
upload bytes there
get the url to it
put it in the userProfile db
store it in the real time db too so we can show it elsewhere easily
Allows us to store images, videos, other files
The real time database pretty much stores Strings. You can't store images there.
Review Everything
Late/missing Problem Sets due on Friday
Final Project due on Monday