├── .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 |
--------------------------------------------------------------------------------