Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Firestore backend DB #4

Merged
merged 17 commits into from
Oct 27, 2023
102 changes: 102 additions & 0 deletions docs/API.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
# RetroRatings back-end API

Our back-end API is a database hosted by Firestore. It has this high-level structure:

## rating-items

- `rating-items`: Map (key-value pairs)
- `<item id>`: Document
- `addedBy`: String (uses auth.currentUser.uid)
- `averageRating`: Number (initialized to 0)
- `dateAdded`: Timestamp (initialized to current time)
- `description`: String
- `image`: String (url of the uploaded image)
- `name`: String
- `ratingCount`: Number (initialized to zero)

The `rating-items` collection is a list of key-value pairs describing each item that can be rated. The key is a unique id (auto-generated, `<item id>` above) and the value is the Document described above.

### Reading

To read items from the database, you must be logged in. As a logged on user, execute the following:

```ts
import { getRatingItems } from "./tasks/getRatingItems"; // replace with the path to getRatingItems.ts relative to the current file

const itemsToReturn = 10; // don't try to read all the items, since there may be a large amount

getRatingItems(itemsToReturn)
.then((returnedItems) => {
// will return an object of the rating items
})
.catch((err) => {
// handle the error here
});
```

### Writing

To add a new item to the database, you must be logged in. As a logged in user, execute the following:

```ts
import { addRatingItem } from "./tasks/addItem"; // replace with the path to addItem.ts relative to the current file

const itemName = "my new item";
const itemDescription = "this is a sample item!";
// const image = <some file object>;

addRatingItem(itemName, itemDescription, image)
.then(() => {
// there should be no output on success
})
.catch((err) => {
// do something with the error here
});
```

## user-ratings

- `user-ratings`: Map (key-value pairs)
- `<user id>`: Array
- `<item id>`: Number

The `user-ratings` collection is a list of key-value pairs that hold all of the site's ratings per user. For each entry in the list, the key is the user's unique id (you can get this with `auth.currentUser?.uid`) and the value is the numerical rating that the user has chosen.

### Reading

To read a user's rating, you must be logged in. As a logged in user, execute the following:

```ts
import { getUserRatings } from "./tasks/getUserRatings"; // replace with the path to getUserRatings.ts relative to the current file

const userId = auth.currentUser.uid; // replace this with the user's id

getUserRatings(userId)
.then((returnedRatings) => {
// will return an object of the user's ratings, or undefined if they haven't rated anything
})
.catch((err) => {
// handle the error here
});
```

### Writing

To add a rating, you must be logged in. To add a rating for the current user:

```ts
import { addRating } from "./tasks/addRating"; // replace with the path to addRating.ts relative to the current file

const id = "abcd"; // replace with the id of the item to rate
const ratingVal = 5; // replace with the selected rating

addRating(id, ratingVal)
.then(() => {
// returns nothing on success
})
.catch((err) => {
// handle the error here
});
```

The average rating of a rating item will be updated whenever a user creates, updates, or deletes a rating.
50 changes: 44 additions & 6 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,11 @@
"preview": "vite preview"
},
"dependencies": {
"@types/dompurify": "^3.0.4",
"bootstrap": "^5.3.2",
"dompurify": "^3.0.6",
"firebase": "^10.5.0",
"nanoid": "^5.0.2",
"react": "^18.2.0",
"react-bootstrap": "^2.9.0",
"react-dom": "^18.2.0",
Expand Down
4 changes: 3 additions & 1 deletion src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import { useState } from "react";
import { signOut } from "firebase/auth";
import { AuthCard } from "./components/AuthCard";
import { auth } from "./config/firebase";
// import { AddNewItemCard } from "./components/AddNewItemCard";
// import { addRatingItem } from "./tasks/addItem";

import Button from "react-bootstrap/Button";
// import Stack from "react-bootstrap/Stack"; // Not used
Expand Down Expand Up @@ -50,7 +52,7 @@ function App() {
</Modal.Header>

<Modal.Body>
<AuthCard actionType={authActionType} noBorder={true}/>
<AuthCard actionType={authActionType} noBorder={true} />
</Modal.Body>

<Modal.Footer>
Expand Down
155 changes: 155 additions & 0 deletions src/components/AddNewItemCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
import Card from "react-bootstrap/Card";
import Form from "react-bootstrap/Form";
import Button from "react-bootstrap/Button";
import Modal from "react-bootstrap/Modal";
import Stack from "react-bootstrap/Stack";
import { useForm, SubmitHandler } from "react-hook-form";
import DOMPurify from "dompurify";
import { useState } from "react";
import { FirebaseError } from "firebase/app";

/**
* Type of the object that is created when the form is submitted
*/
type AddNewItemFormInput = {
itemName: string;
description: string;
image: FileList;
};

/**
* Type of the callback function that will be called with the form's data on submit
*/
type OnFormSubmitHandlerType = (
item: string,
description: string,
image: File
) => Promise<void | FirebaseError>;

/**
* Prop type for the AddNewItemCard component
*/
interface AddNewItemCardProps {
OnFormSubmit?: OnFormSubmitHandlerType;
}

/**
* @brief Component used to add new items to rate to the database
* @param props submit handler used to add items to the database
*/
export const AddNewItemCard = (props: AddNewItemCardProps) => {
// Hook used to handle form submit
const {
register,
handleSubmit,
//reset,
//watch,
//formState: { errors },
} = useForm<AddNewItemFormInput>();

// Function called when the form is submitted
const onAddNewItemFormSubmit: SubmitHandler<AddNewItemFormInput> = async (
data
) => {
// call the callback handler from props if it exists
if (props.OnFormSubmit) {
props
.OnFormSubmit(
DOMPurify.sanitize(data.itemName),
DOMPurify.sanitize(data.description),
data.image[0]
)
.then(() => {
setModalHeader("Success");
setModalText("Item added successfully!");
setShowModal(true);
})
.catch((err) => {
setModalHeader("Error");
switch (err.code) {
case "permission-denied":
setModalText("Error: Only logged in users can add items!");
break;
default:
setModalText(JSON.stringify(err));
break;
}

setShowModal(true);
});
}
};

const [showModal, setShowModal] = useState(false);
const [modalText, setModalText] = useState("");
const [modalHeader, setModalHeader] = useState("");

return (
<>
<Modal show={showModal} onHide={() => setShowModal(false)}>
<Modal.Header closeButton>
<Modal.Title>{modalHeader}</Modal.Title>
</Modal.Header>
<Modal.Body>{modalText}</Modal.Body>
<Modal.Footer>
<Button variant="primary" onClick={() => setShowModal(false)}>
Close
</Button>
</Modal.Footer>
</Modal>

<Card className="w-50">
<Card.Body>
<Card.Title>Add New Item</Card.Title>
<hr />
<Form
onSubmit={handleSubmit(onAddNewItemFormSubmit)}
className="mb-2"
>
<Form.Group controlId="name-input-group" className="mb-3">
<Form.Label>Name</Form.Label>
<Form.Control
required
placeholder="Item name"
{...register("itemName", {})}
/>
</Form.Group>

<Form.Group controlId="desc-input-group" className="mb-3">
<Form.Label>Description</Form.Label>
<Form.Control
required
as="textarea"
rows={5}
{...register("description", {})}
/>
</Form.Group>

<Form.Group controlId="image-upload-group" className="mb-3">
<Form.Label>Image</Form.Label>
<Form.Control
required
type="file"
accept="image/*"
{...register("image")}
></Form.Control>
</Form.Group>

<Stack
direction="horizontal"
gap={3}
className="justify-content-end"
>
<Button variant="danger" type="reset">
Reset
</Button>
<Button variant="primary" type="submit">
Submit
</Button>
</Stack>
</Form>
</Card.Body>
</Card>
</>
);
};
Loading