Skip to content

Commit

Permalink
update add-item to manage list. update firestore.js to include necess…
Browse files Browse the repository at this point in the history
…ary functions for managing users.
  • Loading branch information
jeremiahfallin committed Dec 7, 2023
1 parent 43e28e2 commit 5120bad
Show file tree
Hide file tree
Showing 7 changed files with 151 additions and 24 deletions.
35 changes: 23 additions & 12 deletions src/App.jsx
Original file line number Diff line number Diff line change
@@ -1,41 +1,52 @@
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';

import { AddItem, Home, Layout, List } from './views';
import { Home, Layout, List, ManageList } from './views';

import { useShoppingListData } from './api';

import { useStateWithStorage } from './utils';

export function App() {
/**
* This custom hook takes a token pointing to a shopping list
* This custom hook takes the path of a shopping list
* in our database and syncs it with localStorage for later use.
* Check ./utils/hooks.js for its implementation.
*
* We use `my test list` by default so we can see the list
* of items that was prepopulated for this project.
* We'll later set this to `null` by default (since new users do not
* have tokens), and use `setListToken` when we allow a user
* to create and join a new list.
* We'll later use `setListPath` when we allow a user
* to create and switch between lists.
*/
const [listToken, setListToken] = useStateWithStorage(
'tcl-shopping-list-token',
'my test list',
const [listPath, setListPath] = useStateWithStorage(
'tcl-shopping-list-path',
null,
);

/**
* This custom hook holds info about the current signed in user.
* Check ./api/useAuth.jsx for its implementation.
*/
const { user } = useAuth();
const userId = user?.uid;
const userEmail = user?.email;

/**
* This custom hook takes a user ID and email and fetches
* the shopping lists that the user has access to.
* Check ./api/firestore.js for its implementation.
*/
const lists = useShoppingLists(userId, userEmail);
/**
* This custom hook takes our token and fetches the data for our list.
* Check ./api/firestore.js for its implementation.
*/
const data = useShoppingListData(listToken);
const data = useShoppingListData(listPath);

return (
<Router>
<Routes>
<Route path="/" element={<Layout />}>
<Route index element={<Home />} />
<Route path="/list" element={<List data={data} />} />
<Route path="/add-item" element={<AddItem />} />
<Route path="/manage-list" element={<ManageList />} />
</Route>
</Routes>
</Router>
Expand Down
113 changes: 106 additions & 7 deletions src/api/firebase.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,24 +3,59 @@ import { useEffect, useState } from 'react';
import { db } from './config';
import { getFutureDate } from '../utils';

/**
* A custom hook that subscribes to the user's shopping lists in our Firestore
* database and returns new data whenever the lists change.
* @param {string | null} userId
* @param {string | null} userEmail
* @returns
*/
export function useShoppingLists(userId, userEmail) {
// Start with an empty array for our data.
const initialState = [];
const [data, setData] = useState(initialState);

useEffect(() => {
// If we don't have a userId or userEmail (the user isn't signed in),
// we can't get the user's lists.
if (!userId || !userEmail) return;

// When we get a userEmail, we use it to subscribe to real-time updates
const userDocRef = doc(db, 'users', userEmail);

onSnapshot(userDocRef, (docSnap) => {
if (docSnap.exists()) {
const listRefs = docSnap.data().sharedLists;
const newData = listRefs.map((listRef) => {
return { name: listRef.id, path: listRef.path };
});
setData(newData);
}
});

}, [userId, userEmail]);

return data;
}

/**
* A custom hook that subscribes to a shopping list in our Firestore database
* and returns new data whenever the list changes.
* @param {string | null} listId
* @param {string | null} listPath
* @see https://firebase.google.com/docs/firestore/query-data/listen
*/
export function useShoppingListData(listId) {
export function useShoppingListData(listPath) {
// Start with an empty array for our data.
/** @type {import('firebase/firestore').DocumentData[]} */
const initialState = [];
const [data, setData] = useState(initialState);

useEffect(() => {
if (!listId) return;
if (!listPath) return;

// When we get a listId, we use it to subscribe to real-time updates
// from Firestore.
return onSnapshot(collection(db, listId), (snapshot) => {
return onSnapshot(collection(db, listPath, 'items'), (snapshot) => {
// The snapshot is a real-time update. We iterate over the documents in it
// to get the data.
const nextData = snapshot.docs.map((docSnapshot) => {
Expand All @@ -43,15 +78,79 @@ export function useShoppingListData(listId) {
return data;
}

/**
* Add a new user to the users collection in Firestore.
* @param {Object} user The user object from Firebase Auth.
*/
export async function addUserToDatabase(user) {
// Check if the user already exists in the database.
const userDoc = await getDoc(doc(db, 'users', user.email));
// If the user already exists, we don't need to do anything.
if (userDoc.exists()) {
return;
} else {
// If the user doesn't exist, add them to the database.
// We'll use the user's email as the document id
// because it's more likely that the user will know their email
// than their uid.
await setDoc(doc(db, 'users', user.email), {
email: user.email,
name: user.displayName,
uid: user.uid,
});
}
}

/**
* Create a new list and add it to a user's lists in Firestore.
* @param {string} userId The id of the user who owns the list.
* @param {string} userEmail The email of the user who owns the list.
* @param {string} listName The name of the new list.
*/
export async function createList(userId, userEmail, listName) {
const listDocRef = doc(db, userId, listName);

await setDoc(listDocRef, {
owner: userId,
});

const userDocumentRef = doc(db, 'users', userEmail);

updateDoc(userDocumentRef, {
sharedLists: arrayUnion(listDocRef),
});
}

/**
* Shares a list with another user.
* @param {string} listPath The path to the list to share.
* @param {string} recipientEmail The email of the user to share the list with.
*/
export async function shareList(listPath, recipientEmail) {
// Get the document for the recipient user.
const usersCollectionRef = collection(db, 'users');
const recipientDoc = await getDoc(doc(usersCollectionRef, recipientEmail));
// If the recipient user doesn't exist, we can't share the list.
if (!recipientDoc.exists()) {
return;
}
// Add the list to the recipient user's sharedLists array.
const listDocumentRef = doc(db, listPath);
const userDocumentRef = doc(db, 'users', recipientEmail);
updateDoc(userDocumentRef, {
sharedLists: arrayUnion(listDocumentRef),
});
}

/**
* Add a new item to the user's list in Firestore.
* @param {string} listId The id of the list we're adding to.
* @param {string} listPath The path of the list we're adding to.
* @param {Object} itemData Information about the new item.
* @param {string} itemData.itemName The name of the item.
* @param {number} itemData.daysUntilNextPurchase The number of days until the user thinks they'll need to buy the item again.
*/
export async function addItem(listId, { itemName, daysUntilNextPurchase }) {
const listCollectionRef = collection(db, listId);
export async function addItem(listPath, { itemName, daysUntilNextPurchase }) {
const listCollectionRef = collection(db, listPath, 'items');
// TODO: Replace this call to console.log with the appropriate
// Firebase function, so this information is sent to your database!
return console.log(listCollectionRef, {
Expand Down
13 changes: 13 additions & 0 deletions src/api/useAuth.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@ import { useEffect, useState } from 'react';
import { auth } from './config.js';
import { GoogleAuthProvider, signInWithRedirect } from 'firebase/auth';

/**
* A button that signs the user in using Google OAuth. When clicked,
* the button redirects the user to the Google OAuth sign-in page.
* After the user signs in, they are redirected back to the app.
*/
export const SignInButton = () => (
<button
type="button"
Expand All @@ -11,12 +16,20 @@ export const SignInButton = () => (
</button>
);

/**
* A button that signs the user out of the app using Firebase Auth.
*/
export const SignOutButton = () => (
<button type="button" onClick={() => auth.signOut()}>
Sign Out
</button>
);

/**
* A custom hook that listens for changes to the user's auth state.
* Check out the Firebase docs for more info on auth listeners:
* @see https://firebase.google.com/docs/auth/web/start#set_an_authentication_state_observer_and_get_user_data
*/
export const useAuth = () => {
const [user, setUser] = useState(null);

Expand Down
3 changes: 0 additions & 3 deletions src/views/AddItem.jsx

This file was deleted.

6 changes: 5 additions & 1 deletion src/views/Home.jsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
import './Home.css';

export function Home() {
export function Home({ lists }) {
return (
<div className="Home">
<p>
Hello from the home (<code>/</code>) page!
</p>
{/**
* TODO: write some JavaScript that renders the `lists` array
* so we can see which lists the user has access to.
*/}
</div>
);
}
3 changes: 3 additions & 0 deletions src/views/ManageItem.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export function ManageList() {
return <p>Hello from the <code>/manage-list</code> page!</p>
}
2 changes: 1 addition & 1 deletion src/views/index.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export * from './AddItem';
export * from './ManageItem';
export * from './Home';
export * from './Layout';
export * from './List';

0 comments on commit 5120bad

Please sign in to comment.