Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added .DS_Store
Binary file not shown.
Binary file added fcm-starter/.DS_Store
Binary file not shown.
Binary file added fcm/.DS_Store
Binary file not shown.
24 changes: 24 additions & 0 deletions mvp-starter/.vscode/launch.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "chrome",
"request": "launch",
"name": "Launch Chrome against localhost",
"url": "http://localhost:8080",
"webRoot": "${workspaceFolder}"
},
{
"name": "SWA: Run mvp-starter",
"request": "launch",
"type": "chrome",
"url": "http://localhost:4280",
"preLaunchTask": "swa: start mvp-starter",
"webRoot": "${workspaceFolder}/",
"timeout": 30000
}
]
}
159 changes: 127 additions & 32 deletions mvp-starter/components/expenseDialog.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,16 @@
*/

import { useState, useEffect } from 'react';
import { Avatar, Button, Dialog, DialogActions, DialogContent, Stack, TextField, Typography } from '@mui/material';
import {
Avatar,
Button,
Dialog,
DialogActions,
DialogContent,
Stack,
TextField,
Typography,
} from '@mui/material';
import AdapterDateFns from '@mui/lab/AdapterDateFns';
import DatePicker from '@mui/lab/DatePicker';
import LocalizationProvider from '@mui/lab/LocalizationProvider';
Expand All @@ -25,18 +34,19 @@ import { addReceipt, updateReceipt } from '../firebase/firestore';
import { replaceImage, uploadImage } from '../firebase/storage';
import { RECEIPTS_ENUM } from '../pages/dashboard';
import styles from '../styles/expenseDialog.module.scss';
import { add } from 'date-fns';

const DEFAULT_FILE_NAME = "No file selected";
const DEFAULT_FILE_NAME = 'No file selected';

// Default form state for the dialog
const DEFAULT_FORM_STATE = {
fileName: DEFAULT_FILE_NAME,
file: null,
date: null,
locationName: "",
address: "",
items: "",
amount: "",
locationName: '',
address: '',
items: '',
amount: '',
};

/*
Expand All @@ -50,6 +60,7 @@ const DEFAULT_FORM_STATE = {
- onCloseDialog emits to close dialog
*/
export default function ExpenseDialog(props) {
const { authUser } = useAuth();
const isEdit = Object.keys(props.edit).length > 0;
const [formFields, setFormFields] = useState(isEdit ? props.edit : DEFAULT_FORM_STATE);
const [isSubmitting, setIsSubmitting] = useState(false);
Expand All @@ -59,73 +70,157 @@ export default function ExpenseDialog(props) {
if (props.showDialog) {
setFormFields(isEdit ? props.edit : DEFAULT_FORM_STATE);
}
}, [props.edit, props.showDialog])
}, [props.edit, props.showDialog]);

// Check whether any of the form fields are unedited
const isDisabled = () => formFields.fileName === DEFAULT_FILE_NAME || !formFields.date || formFields.locationName.length === 0
|| formFields.address.length === 0 || formFields.items.length === 0 || formFields.amount.length === 0;
const isDisabled = () =>
formFields.fileName === DEFAULT_FILE_NAME ||
!formFields.date ||
formFields.locationName.length === 0 ||
formFields.address.length === 0 ||
formFields.items.length === 0 ||
formFields.amount.length === 0;

// Update given field in the form
const updateFormField = (event, field) => {
setFormFields(prevState => ({...prevState, [field]: event.target.value}))
}
setFormFields((prevState) => ({ ...prevState, [field]: event.target.value }));
};

// Set the relevant fields for receipt image
const setFileData = (target) => {
const file = target.files[0];
setFormFields(prevState => ({...prevState, fileName: file.name}));
setFormFields(prevState => ({...prevState, file}));
}
setFormFields((prevState) => ({ ...prevState, fileName: file.name }));
setFormFields((prevState) => ({ ...prevState, file }));
};

const closeDialog = () => {
setIsSubmitting(false);
props.onCloseDialog();
}
};

const handleSubmit = async () => {
setIsSubmitting(true);

try {
if (isEdit) {
if (formFields.fileName) {
await replaceImage(formFields.file, formFields.imageBucket);
}
await updateReceipt(
formFields.id,
authUser.uid,
formFields.date,
formFields.locationName,
formFields.address,
formFields.items,
formFields.amount,
formFields.imageBucket
);
} else {
const bucket = await uploadImage(formFields.file, authUser.uid);
await addReceipt(
authUser.uid,
formFields.date,
formFields.locationName,
formFields.address,
formFields.items,
formFields.amount,
bucket
);
}
props.onSuccess(isEdit ? RECEIPTS_ENUM.edit : RECEIPTS_ENUM.add);
} catch (error) {
props.onError(isEdit ? RECEIPTS_ENUM.edit : RECEIPTS_ENUM.add);
console.log(error);
}
closeDialog();
};

return (
<Dialog classes={{paper: styles.dialog}}
<Dialog
classes={{ paper: styles.dialog }}
onClose={() => closeDialog()}
open={props.showDialog}
component="form">
component="form"
>
<Typography variant="h4" className={styles.title}>
{isEdit ? "EDIT" : "ADD"} EXPENSE
{isEdit ? 'EDIT' : 'ADD'} EXPENSE
</Typography>
<DialogContent className={styles.fields}>
<Stack direction="row" spacing={2} className={styles.receiptImage}>
{(isEdit && !formFields.fileName) && <Avatar alt="receipt image" src={formFields.imageUrl} sx={{ marginRight: '1em' }}/> }
{isEdit && !formFields.fileName && (
<Avatar alt="receipt image" src={formFields.imageUrl} sx={{ marginRight: '1em' }} />
)}
<Button variant="outlined" component="label" color="secondary">
Upload Receipt
<input type="file" hidden onInput={(event) => {setFileData(event.target)}} />
<input
type="file"
hidden
onInput={(event) => {
setFileData(event.target);
}}
/>
</Button>
<Typography>{formFields.fileName}</Typography>
</Stack>
<Stack>
<LocalizationProvider dateAdapter={AdapterDateFns}>
<DatePicker
<DatePicker
label="Date"
value={formFields.date}
onChange={(newDate) => {
setFormFields(prevState => ({...prevState, date: newDate}));
setFormFields((prevState) => ({ ...prevState, date: newDate }));
}}
maxDate={new Date()}
renderInput={(params) => <TextField color="tertiary" {...params} />}
/>
</LocalizationProvider>
</Stack>
<TextField color="tertiary" label="Location name" variant="standard" value={formFields.locationName} onChange={(event) => updateFormField(event, 'locationName')} />
<TextField color="tertiary" label="Location address" variant="standard" value={formFields.address} onChange={(event) => updateFormField(event, 'address')} />
<TextField color="tertiary" label="Items" variant="standard" value={formFields.items} onChange={(event) => updateFormField(event, 'items')} />
<TextField color="tertiary" label="Amount" variant="standard" value={formFields.amount} onChange={(event) => updateFormField(event, 'amount')} />
<TextField
color="tertiary"
label="Location name"
variant="standard"
value={formFields.locationName}
onChange={(event) => updateFormField(event, 'locationName')}
/>
<TextField
color="tertiary"
label="Location address"
variant="standard"
value={formFields.address}
onChange={(event) => updateFormField(event, 'address')}
/>
<TextField
color="tertiary"
label="Items"
variant="standard"
value={formFields.items}
onChange={(event) => updateFormField(event, 'items')}
/>
<TextField
color="tertiary"
label="Amount"
variant="standard"
value={formFields.amount}
onChange={(event) => updateFormField(event, 'amount')}
/>
</DialogContent>
<DialogActions>
{isSubmitting ?
{isSubmitting ? (
<Button color="secondary" variant="contained" disabled={true}>
Submitting...
</Button> :
<Button color="secondary" variant="contained" disabled={isDisabled()}>
</Button>
) : (
<Button
color="secondary"
variant="contained"
disabled={isDisabled()}
onClick={handleSubmit}
>
Submit
</Button>}
</Button>
)}
</DialogActions>
</Dialog>
)
}
);
}
11 changes: 6 additions & 5 deletions mvp-starter/components/navbar.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,20 +20,21 @@ import { useAuth } from '../firebase/auth';
import styles from '../styles/navbar.module.scss';

export default function NavBar() {
const { authUser, signOut } = useAuth();

return (
<Box sx={{ flexGrow: 1 }}>
<AppBar position="static" className={styles.appbar}>
<Toolbar className={styles.toolbar}>
<Container className={styles.container}>
<Typography variant="h3" sx={{ flexGrow: 1, alignSelf: "center" }}>
<Typography variant="h3" sx={{ flexGrow: 1, alignSelf: 'center' }}>
EXPENSE TRACKER
</Typography>
<Stack direction="row" spacing={2} sx={{ alignItems: "center" }}>
<Stack direction="row" spacing={2} sx={{ alignItems: 'center' }}>
<Typography variant="h6" sx={{ flexGrow: 1 }}>
Insert user email here
{authUser?.email}
</Typography>
<Button variant="text" color="secondary">
<Button variant="text" color="secondary" onClick={signOut}>
Logout
</Button>
</Stack>
Expand All @@ -42,4 +43,4 @@ export default function NavBar() {
</AppBar>
</Box>
);
}
}
56 changes: 55 additions & 1 deletion mvp-starter/firebase/auth.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,58 @@
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
*/

import { createContext, useContext, useEffect, useState } from 'react';
import { onAuthStateChanged, signOut as authSignOut } from 'firebase/auth';
import { auth } from './firebase';

export default function useFirebaseAuth() {
const [authUser, setAuthUser] = useState(null);
const [isLoading, setIsLoading] = useState(true);

const clear = () => {
setAuthUser(null);
setIsLoading(false);
};

const authStateChanged = async (user) => {
setIsLoading(true);
if (!user) {
clear();
return;
}
setAuthUser({
uid: user.uid,
email: user.email,
});
setIsLoading(false);
};

const signOut = () => authSignOut(auth).then(clear);

// Listen for Firebase Auth state change
useEffect(() => {
const unsubscribe = onAuthStateChanged(auth, authStateChanged);
return () => unsubscribe();
}, []);

return {
authUser,
isLoading,
signOut,
};
}

const AuthUserContext = createContext({
authUser: null,
isLoading: true,
signOut: async () => {},
});

export function AuthUserProvider({ children }) {
const auth = useFirebaseAuth();
return <AuthUserContext.Provider value={auth}>{children}</AuthUserContext.Provider>;
}

export const useAuth = () => useContext(AuthUserContext);
25 changes: 25 additions & 0 deletions mvp-starter/firebase/firebase.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,28 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// Import the functions you need from the SDKs you need

import { initializeApp } from 'firebase/app';
import { getAuth } from 'firebase/auth';
import { getFirestore } from 'firebase/firestore';
import { getStorage } from 'firebase/storage';

const firebaseConfig = {
apiKey: 'number',
authDomain: 'ololand-project.firebaseapp.com',
databaseURL: 'https://ololand-project-default-rtdb.firebaseio.com',
projectId: 'ololand-project',
storageBucket: 'ololand-project.appspot.com',
messagingSenderId: '860252520082',
appId: '1:860252520082:web:663f8c94d2da7d9ea9b04b',
measurementId: 'G-KWBSXY1NM9',
};

// Initialize Firebase
initializeApp(firebaseConfig);

// Initialize Firebase Authentication and get a reference to the service
export const auth = getAuth();
export const db = getFirestore();
export const storage = getStorage();
Loading