├── .gitignore ├── README.md ├── package-lock.json ├── package.json ├── public ├── favicon.ico ├── index.html ├── logo192.png ├── logo512.png ├── manifest.json └── robots.txt ├── src ├── App.js ├── components │ ├── Expenses.js │ ├── Header.js │ ├── Participants.js │ └── Payments.js ├── config │ └── firebase.js ├── index.js ├── mockExpenses.js ├── pages │ ├── Authenticate.js │ ├── Home.js │ └── SharedExpense.js └── setupTests.js └── yarn.lock /.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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Firebase Workshop 2 | 3 | Follow the steps in Notion! This README will be updated after the workshop finishes 🦄 4 | 5 | ## For those who attended the workshop 6 | 7 | Thanks so much for your time and feeback! 🎉 I hope you liked it and got some useful knowledge for your future as developers! 8 | 9 | **Every line of code written in the workshop is available in these two branches:** 10 | 11 | - workshop-day-1 12 | - workshop-day-2 13 | 14 | ## Solutions 15 | 16 | I developed some solutions really similar to what we did in the workshop, which are stored in these branches: 17 | 18 | - solution-day-1 19 | - solution-day-2 20 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "workshop-firebase", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@chakra-ui/icons": "^1.0.14", 7 | "@chakra-ui/react": "^1.6.5", 8 | "@emotion/react": "^11.4.0", 9 | "@emotion/styled": "^11.3.0", 10 | "@testing-library/jest-dom": "^5.11.4", 11 | "@testing-library/react": "^11.1.0", 12 | "@testing-library/user-event": "^12.1.10", 13 | "firebase": "^8.7.1", 14 | "framer-motion": "^4.1.17", 15 | "react": "^17.0.2", 16 | "react-dom": "^17.0.2", 17 | "react-router-dom": "^5.2.0", 18 | "react-scripts": "4.0.3", 19 | "uuid": "^8.3.2", 20 | "web-vitals": "^1.0.1" 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 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ccastillo06/firebase-workshop/901037f2dccaac8d059ebc06549ef5f136c874db/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 14 | Split & Share 15 | 16 | 17 | 18 |
19 | 20 | 21 | -------------------------------------------------------------------------------- /public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ccastillo06/firebase-workshop/901037f2dccaac8d059ebc06549ef5f136c874db/public/logo192.png -------------------------------------------------------------------------------- /public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ccastillo06/firebase-workshop/901037f2dccaac8d059ebc06549ef5f136c874db/public/logo512.png -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | import { Redirect, Route, Switch } from 'react-router'; 3 | import { Box } from '@chakra-ui/layout'; 4 | 5 | import Header from './components/Header'; 6 | import Home from './pages/Home'; 7 | import SharedExpense from './pages/SharedExpense'; 8 | import Authenticate from './pages/Authenticate'; 9 | 10 | import { mockAllExpenses } from './mockExpenses'; 11 | 12 | function App() { 13 | const [user, setUser] = useState(null); 14 | // TODO: Change to load all user expenses from Firestore 15 | const [allExpenses, setAllExpenses] = useState(mockAllExpenses); 16 | 17 | return ( 18 | 19 |
setUser(null)} /> 20 | 21 | 22 | 23 | 24 | {user ? ( 25 | 26 | ) : ( 27 | 28 | )} 29 | 30 | 31 | 32 | {user ? ( 33 | 34 | ) : ( 35 | 36 | )} 37 | 38 | 39 | 40 | 41 | ); 42 | } 43 | 44 | export default App; 45 | -------------------------------------------------------------------------------- /src/components/Expenses.js: -------------------------------------------------------------------------------- 1 | import { Fragment } from 'react'; 2 | import { 3 | Box, 4 | Heading, 5 | List, 6 | ListIcon, 7 | ListItem, 8 | Text, 9 | } from '@chakra-ui/layout'; 10 | import { SmallAddIcon } from '@chakra-ui/icons'; 11 | import { 12 | Button, 13 | Divider, 14 | Drawer, 15 | DrawerBody, 16 | DrawerContent, 17 | DrawerHeader, 18 | DrawerOverlay, 19 | FormControl, 20 | FormLabel, 21 | Input, 22 | NumberDecrementStepper, 23 | NumberIncrementStepper, 24 | NumberInput, 25 | NumberInputField, 26 | NumberInputStepper, 27 | Select, 28 | useDisclosure, 29 | } from '@chakra-ui/react'; 30 | 31 | function Expenses({ expenseList, handleSaveExpense, participantList }) { 32 | const { isOpen, onOpen, onClose } = useDisclosure(); 33 | 34 | return ( 35 | <> 36 | 37 | 38 | Gastos por persona 💶 39 | 40 | 41 | 42 | {(expenseList || []).map((expense, index) => ( 43 | 44 | 45 | 46 | 47 | {expense.title} 48 | 49 | Cantidad: {expense.price} € 50 | 51 | 52 | Pagado por: {expense.paidBy.name} 53 | 54 | 55 | 56 | 57 | 58 | ))} 59 | 60 | 61 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | Nuevo gasto 71 | 72 | { 75 | e.preventDefault(); 76 | handleSaveExpense(e); 77 | onClose(); 78 | }} 79 | > 80 | 81 | ¿Quién ha pagado? 82 | 83 | 90 | 91 | 92 | 93 | ¿Cuánto ha costado? 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | ¿Qué habéis comprado? 106 | 107 | 108 | 109 | {/* TODO: Add new picture field to use Firestore Storage */} 110 | 111 | 114 | 115 | 116 | 117 | 118 | 119 | ); 120 | } 121 | 122 | export default Expenses; 123 | -------------------------------------------------------------------------------- /src/components/Header.js: -------------------------------------------------------------------------------- 1 | import { Link } from 'react-router-dom'; 2 | import { Box, Flex } from '@chakra-ui/layout'; 3 | import { Button, Heading } from '@chakra-ui/react'; 4 | 5 | function Header({ hasUser, onLogout }) { 6 | return ( 7 | 8 | 9 | 10 | Split & Share 11 | 12 | 13 | {hasUser ? : null} 14 | 15 | 16 | 17 | En colaboración con Upgrade Hub 18 | 19 | 20 | ); 21 | } 22 | 23 | export default Header; 24 | -------------------------------------------------------------------------------- /src/components/Participants.js: -------------------------------------------------------------------------------- 1 | import { Box, Heading, ListItem, UnorderedList } from '@chakra-ui/layout'; 2 | import { 3 | Button, 4 | Drawer, 5 | DrawerBody, 6 | DrawerContent, 7 | DrawerHeader, 8 | DrawerOverlay, 9 | FormControl, 10 | FormLabel, 11 | Input, 12 | useDisclosure, 13 | } from '@chakra-ui/react'; 14 | 15 | function Participants({ participantList, handleSaveParticipant }) { 16 | const { isOpen, onOpen, onClose } = useDisclosure(); 17 | 18 | return ( 19 | <> 20 | 21 | 22 | Lista de miembros 🦹‍♂️🕵️🥷 23 | 24 | 25 | 26 | {participantList.map((participant) => ( 27 | {participant.name} 28 | ))} 29 | 30 | 31 | 34 | 35 | 36 | 37 | 38 | 39 | Nuevo miembro 40 | 41 | { 44 | e.preventDefault(); 45 | handleSaveParticipant(e); 46 | onClose(); 47 | }} 48 | > 49 | 50 | Nombre 51 | 52 | 53 | 54 | 57 | 58 | 59 | 60 | 61 | 62 | ); 63 | } 64 | 65 | export default Participants; 66 | -------------------------------------------------------------------------------- /src/components/Payments.js: -------------------------------------------------------------------------------- 1 | import { Fragment } from 'react'; 2 | import { Box, Heading, List, ListItem, Text } from '@chakra-ui/layout'; 3 | import { Divider } from '@chakra-ui/react'; 4 | 5 | function Payments({ expenseList, participantList }) { 6 | const initialExpenses = participantList.reduce( 7 | (acc, next) => ({ 8 | ...acc, 9 | [next.id]: { 10 | ...next, 11 | total: 0, 12 | }, 13 | }), 14 | {} 15 | ); 16 | 17 | const totalExpenses = expenseList.reduce((acc, next) => { 18 | return { 19 | ...acc, 20 | [next.paidBy.id]: { 21 | ...(acc[next.paidBy.id] || {}), 22 | total: acc[next.paidBy.id].total + next.price, 23 | }, 24 | }; 25 | }, initialExpenses); 26 | 27 | const totalPaid = expenseList.reduce((acc, next) => acc + next.price, 0); 28 | const paymentPerPerson = totalPaid / participantList.length; 29 | 30 | const totalWithDebts = Object.keys(totalExpenses).reduce((acc, next) => { 31 | return { 32 | ...acc, 33 | [next]: { 34 | ...totalExpenses[next], 35 | owns: paymentPerPerson - totalExpenses[next].total, 36 | }, 37 | }; 38 | }, {}); 39 | 40 | return ( 41 | 42 | 43 | Ajuste de cuentas 💸💸💸 44 | 45 | 46 | 47 | 48 | ¡Recuerda! 49 | 50 | Valor en positivo - Debes dinerito 😭 51 | Valor en negativo - ¡Te deben dinerito! 🦄 52 | 53 | 54 | 55 | {Object.keys(totalWithDebts).map((userId) => ( 56 | 57 | 58 | 59 | {totalWithDebts[userId].name} 60 | 61 | 62 | Total pagado: {totalWithDebts[userId].total.toFixed(2)} € 63 | 64 | 65 | A deber: {totalWithDebts[userId].owns.toFixed(2)} € 66 | 67 | 68 | 69 | 70 | 71 | ))} 72 | 73 | 74 | ); 75 | } 76 | 77 | export default Payments; 78 | -------------------------------------------------------------------------------- /src/config/firebase.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ccastillo06/firebase-workshop/901037f2dccaac8d059ebc06549ef5f136c874db/src/config/firebase.js -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import { BrowserRouter } from 'react-router-dom'; 4 | import { ChakraProvider } from '@chakra-ui/react'; 5 | 6 | import App from './App'; 7 | 8 | ReactDOM.render( 9 | 10 | 11 | 12 | 13 | 14 | 15 | , 16 | document.getElementById('root') 17 | ); 18 | -------------------------------------------------------------------------------- /src/mockExpenses.js: -------------------------------------------------------------------------------- 1 | import { v4 as uuid } from 'uuid'; 2 | 3 | export const getNewExpense = (title) => ({ 4 | id: uuid(), 5 | title, 6 | expenses: [], 7 | participants: [], 8 | }); 9 | 10 | export const mockAllExpenses = [ 11 | { 12 | id: '012-345-678', 13 | title: 'Cumpleaños de Keanu Reeves', 14 | expenses: [ 15 | { 16 | id: uuid(), 17 | title: 'Zapatillas de deporte', 18 | price: 120, 19 | paidBy: { id: '111-111-111', name: 'juan' }, 20 | }, 21 | { 22 | id: uuid(), 23 | title: 'Cena de grupo', 24 | price: 440, 25 | paidBy: { id: '222-222-222', name: 'claudia' }, 26 | }, 27 | { 28 | id: uuid(), 29 | title: 'Entradas discoteca', 30 | price: 85, 31 | paidBy: { id: '111-111-111', name: 'juan' }, 32 | }, 33 | { 34 | id: uuid(), 35 | title: 'Chupitos de Jagger', 36 | price: 30, 37 | paidBy: { id: '333-333-333', name: 'pepe' }, 38 | }, 39 | ], 40 | participants: [ 41 | { id: '111-111-111', name: 'juan' }, 42 | { id: '222-222-222', name: 'claudia' }, 43 | { id: '333-333-333', name: 'pepe' }, 44 | ], 45 | }, 46 | { 47 | id: '014-345-678', 48 | title: 'Cumpleaños de Wonder Woman', 49 | expenses: [], 50 | participants: [ 51 | { id: '111-111-111', name: 'bruce' }, 52 | { id: '222-222-222', name: 'clark' }, 53 | { id: '333-333-333', name: 'steve' }, 54 | ], 55 | }, 56 | ]; 57 | -------------------------------------------------------------------------------- /src/pages/Authenticate.js: -------------------------------------------------------------------------------- 1 | import { Button } from '@chakra-ui/button'; 2 | import { FormControl, FormLabel } from '@chakra-ui/form-control'; 3 | import { Input } from '@chakra-ui/input'; 4 | import { Box, Heading } from '@chakra-ui/layout'; 5 | import { useState } from 'react'; 6 | 7 | function Authenticate({ onLogin }) { 8 | const [isLogin, setIsLogin] = useState(true); 9 | 10 | // TODO: Authenticate via Firebase Authentication methods (Login/Password & Google) 11 | function handleSubmit(e) { 12 | e.preventDefault(); 13 | 14 | const email = e.target[0].value; 15 | const password = e.target[1].value; 16 | 17 | onLogin({ email, password }); 18 | } 19 | 20 | function handleGoogleLogin() { 21 | // TODO: Add google login with Firebase Authentication 22 | } 23 | 24 | return ( 25 | 26 | 27 | Autentícate para entrar 28 | 29 | 30 | 31 | 32 | Correo electrónico 33 | 34 | 35 | 36 | 42 | Contraseña 43 | 44 | 45 | 46 | 49 | 50 | 51 | 54 | 55 | ); 56 | } 57 | 58 | export default Authenticate; 59 | -------------------------------------------------------------------------------- /src/pages/Home.js: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | import { Button } from '@chakra-ui/button'; 4 | import { Box, Divider, Heading, Stack, Text } from '@chakra-ui/layout'; 5 | import { FormControl, FormLabel } from '@chakra-ui/form-control'; 6 | import { Input } from '@chakra-ui/input'; 7 | 8 | import { getNewExpense } from '../mockExpenses'; 9 | 10 | function Home({ allExpenses, setAllExpenses }) { 11 | function handleAddExpense(title) { 12 | setAllExpenses([...allExpenses, getNewExpense(title)]); 13 | } 14 | 15 | return ( 16 | 17 | 18 | Todos mis gastos 19 | 20 | 21 | 22 | {allExpenses.map((expense) => ( 23 | 24 | 25 | {expense.title} 26 | 27 | Participantes: {expense.participants.length} 28 | 29 | 30 | 31 | ))} 32 | 33 | 34 | 35 | 36 | 37 | { 40 | e.preventDefault(); 41 | 42 | const title = e.target[0].value.trim().toLowerCase(); 43 | handleAddExpense(title); 44 | 45 | e.target.reset(); 46 | }} 47 | > 48 | 49 | Título 50 | 51 | 52 | 53 | 56 | 57 | 58 | 59 | ); 60 | } 61 | 62 | export default Home; 63 | -------------------------------------------------------------------------------- /src/pages/SharedExpense.js: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | import { useParams } from 'react-router-dom'; 3 | import { Box, Heading } from '@chakra-ui/layout'; 4 | import { Spinner } from '@chakra-ui/spinner'; 5 | import { v4 as uuid } from 'uuid'; 6 | 7 | import Participants from '../components/Participants'; 8 | import Expenses from '../components/Expenses'; 9 | import Payments from '../components/Payments'; 10 | 11 | function SharedExpense({ allExpenses }) { 12 | const [sharedExpenses, setSharedExpenses] = useState(null); 13 | const { id } = useParams(); 14 | 15 | useEffect(() => { 16 | // TODO: Change this so it uses Firestore to load the value 17 | const pageExpenses = allExpenses.find((e) => e.id === id); 18 | setSharedExpenses(pageExpenses); 19 | }, [id]); 20 | 21 | function handleSaveParticipant(e) { 22 | setSharedExpenses({ 23 | ...sharedExpenses, 24 | participants: [ 25 | ...sharedExpenses.participants, 26 | { 27 | id: uuid(), 28 | name: e.target[0].value.toLowerCase(), 29 | }, 30 | ], 31 | }); 32 | 33 | // TODO: Save in Firestore 34 | } 35 | 36 | function handleSaveExpense(e) { 37 | const userId = e.target[0].value; 38 | const price = Number(e.target[1].value); 39 | const title = e.target[2].value.trim().toLowerCase(); 40 | 41 | setSharedExpenses({ 42 | ...sharedExpenses, 43 | expenses: [ 44 | ...sharedExpenses.expenses, 45 | { 46 | id: uuid(), 47 | title, 48 | price, 49 | paidBy: sharedExpenses.participants.find((p) => p.id === userId), 50 | }, 51 | ], 52 | }); 53 | 54 | // TODO: Save in Firestore 55 | } 56 | 57 | if (!sharedExpenses) { 58 | return ( 59 | 60 | 61 | 62 | ); 63 | } 64 | 65 | return ( 66 | 67 | 68 | {sharedExpenses.title} 69 | 70 | 71 | 76 | 77 | 81 | 82 | 86 | 87 | ); 88 | } 89 | 90 | export default SharedExpense; 91 | -------------------------------------------------------------------------------- /src/setupTests.js: -------------------------------------------------------------------------------- 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 | --------------------------------------------------------------------------------