From 7e11e4e720c6902cc455da13f408d15ed90591a9 Mon Sep 17 00:00:00 2001
From: Collin <88743846+PaizCollin@users.noreply.github.com>
Date: Thu, 25 May 2023 10:56:05 -0700
Subject: [PATCH] adds web-app files
includes all necessary files (that can be viewed publicly) for the web-application
---
.gitignore | 13 +
README.md | 2457 +++++++++++++++++
backend/config/db.js | 15 +
backend/controllers/apiary.controller.js | 594 ++++
backend/controllers/data.controller.js | 88 +
backend/controllers/user.controller.js | 101 +
backend/middleware/auth.middleware.js | 36 +
backend/middleware/error.middleware.js | 14 +
backend/models/apiary.model.js | 110 +
backend/models/data.model.js | 45 +
backend/models/user.model.js | 24 +
backend/routes/apiary.routes.js | 51 +
backend/routes/data.routes.js | 7 +
backend/routes/user.routes.js | 13 +
backend/server.js | 45 +
frontend/.gitignore | 23 +
frontend/README.md | 46 +
frontend/package.json | 68 +
frontend/public/about-ss.PNG | Bin 0 -> 64242 bytes
frontend/public/db-ss.PNG | Bin 0 -> 249602 bytes
frontend/public/faq-ss.PNG | Bin 0 -> 100506 bytes
frontend/public/favicon.ico | Bin 0 -> 3585 bytes
frontend/public/index.html | 43 +
frontend/public/logo192.png | Bin 0 -> 4153 bytes
frontend/public/logo512.png | Bin 0 -> 12066 bytes
frontend/public/manage-ss.PNG | Bin 0 -> 61086 bytes
frontend/public/manifest.json | 25 +
frontend/public/robots.txt | 2 +
frontend/public/signin-ss.PNG | Bin 0 -> 33826 bytes
frontend/public/signup-ss.PNG | Bin 0 -> 36211 bytes
frontend/src/App.js | 131 +
frontend/src/App.test.js | 15 +
frontend/src/app/store.js | 10 +
frontend/src/components/AboutCard.jsx | 107 +
frontend/src/components/AddApiaryCard.jsx | 147 +
frontend/src/components/AddDeviceCard.jsx | 162 ++
frontend/src/components/AddUserCard.jsx | 154 ++
frontend/src/components/ApiaryCard.jsx | 323 +++
frontend/src/components/AutocompleteMaps.tsx | 185 ++
frontend/src/components/CustomTooltip.jsx | 29 +
frontend/src/components/DeviceCard.jsx | 220 ++
frontend/src/components/FAQCard.jsx | 107 +
frontend/src/components/Graph.jsx | 299 ++
frontend/src/components/Header.jsx | 24 +
frontend/src/components/Loading.jsx | 13 +
frontend/src/components/Overview.jsx | 378 +++
frontend/src/components/SelectApiary.jsx | 61 +
frontend/src/components/Sidebar.jsx | 304 ++
frontend/src/components/Topbar.jsx | 125 +
frontend/src/components/UserCard.jsx | 222 ++
frontend/src/custom.scss | 4 +
.../src/features/apiary/apiary.service.js | 223 ++
frontend/src/features/apiary/apiary.slice.js | 425 +++
frontend/src/features/auth/auth.service.js | 38 +
frontend/src/features/auth/auth.slice.js | 99 +
frontend/src/index.css | 40 +
frontend/src/index.html | 0
frontend/src/index.js | 23 +
frontend/src/pages/About.jsx | 83 +
frontend/src/pages/Dashboard.jsx | 154 ++
frontend/src/pages/FAQ.jsx | 117 +
frontend/src/pages/Login.jsx | 227 ++
frontend/src/pages/Manage.jsx | 136 +
frontend/src/pages/Register.jsx | 255 ++
frontend/src/reportWebVitals.js | 13 +
frontend/src/setupTests.js | 5 +
frontend/src/theme.js | 389 +++
package.json | 32 +
68 files changed, 9099 insertions(+)
create mode 100644 .gitignore
create mode 100644 README.md
create mode 100644 backend/config/db.js
create mode 100644 backend/controllers/apiary.controller.js
create mode 100644 backend/controllers/data.controller.js
create mode 100644 backend/controllers/user.controller.js
create mode 100644 backend/middleware/auth.middleware.js
create mode 100644 backend/middleware/error.middleware.js
create mode 100644 backend/models/apiary.model.js
create mode 100644 backend/models/data.model.js
create mode 100644 backend/models/user.model.js
create mode 100644 backend/routes/apiary.routes.js
create mode 100644 backend/routes/data.routes.js
create mode 100644 backend/routes/user.routes.js
create mode 100644 backend/server.js
create mode 100644 frontend/.gitignore
create mode 100644 frontend/README.md
create mode 100644 frontend/package.json
create mode 100644 frontend/public/about-ss.PNG
create mode 100644 frontend/public/db-ss.PNG
create mode 100644 frontend/public/faq-ss.PNG
create mode 100644 frontend/public/favicon.ico
create mode 100644 frontend/public/index.html
create mode 100644 frontend/public/logo192.png
create mode 100644 frontend/public/logo512.png
create mode 100644 frontend/public/manage-ss.PNG
create mode 100644 frontend/public/manifest.json
create mode 100644 frontend/public/robots.txt
create mode 100644 frontend/public/signin-ss.PNG
create mode 100644 frontend/public/signup-ss.PNG
create mode 100644 frontend/src/App.js
create mode 100644 frontend/src/App.test.js
create mode 100644 frontend/src/app/store.js
create mode 100644 frontend/src/components/AboutCard.jsx
create mode 100644 frontend/src/components/AddApiaryCard.jsx
create mode 100644 frontend/src/components/AddDeviceCard.jsx
create mode 100644 frontend/src/components/AddUserCard.jsx
create mode 100644 frontend/src/components/ApiaryCard.jsx
create mode 100644 frontend/src/components/AutocompleteMaps.tsx
create mode 100644 frontend/src/components/CustomTooltip.jsx
create mode 100644 frontend/src/components/DeviceCard.jsx
create mode 100644 frontend/src/components/FAQCard.jsx
create mode 100644 frontend/src/components/Graph.jsx
create mode 100644 frontend/src/components/Header.jsx
create mode 100644 frontend/src/components/Loading.jsx
create mode 100644 frontend/src/components/Overview.jsx
create mode 100644 frontend/src/components/SelectApiary.jsx
create mode 100644 frontend/src/components/Sidebar.jsx
create mode 100644 frontend/src/components/Topbar.jsx
create mode 100644 frontend/src/components/UserCard.jsx
create mode 100644 frontend/src/custom.scss
create mode 100644 frontend/src/features/apiary/apiary.service.js
create mode 100644 frontend/src/features/apiary/apiary.slice.js
create mode 100644 frontend/src/features/auth/auth.service.js
create mode 100644 frontend/src/features/auth/auth.slice.js
create mode 100644 frontend/src/index.css
create mode 100644 frontend/src/index.html
create mode 100644 frontend/src/index.js
create mode 100644 frontend/src/pages/About.jsx
create mode 100644 frontend/src/pages/Dashboard.jsx
create mode 100644 frontend/src/pages/FAQ.jsx
create mode 100644 frontend/src/pages/Login.jsx
create mode 100644 frontend/src/pages/Manage.jsx
create mode 100644 frontend/src/pages/Register.jsx
create mode 100644 frontend/src/reportWebVitals.js
create mode 100644 frontend/src/setupTests.js
create mode 100644 frontend/src/theme.js
create mode 100644 package.json
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..120b985
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,13 @@
+node_modules
+.env
+package-lock.json
+
+# OS generated files #
+######################
+.DS_Store
+.DS_Store?
+._*
+.Spotlight-V100
+.Trashes
+ehthumbs.db
+Thumbs.db
\ No newline at end of file
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..b2f953f
--- /dev/null
+++ b/README.md
@@ -0,0 +1,2457 @@
+# Beehive Monitor Web Application
+
+This full stack web application (_MERN_ Stack), was built to service the Santa Clara University Senior Design engineering project: **A Hardware Solution to Wireless Beehive Monitoring**
+
+The frontend is built using _React_, _Redux_, and _Axios_; the backend is built using _Express_ and _Node_; and the database is managed in _MongoDB_. The frontend heavily utilizes the open-source _React_ component library, _Material UI_ for styling and design. The backend utilizes _Mongoose_ for database modeling and management.
+
+This project aims to rely solely on _open-source_ technologies to ensure its reliability and longevity for beekeepers around the world.
+
+# Table of Contents
+
+- [Beehive Monitor Web Application](#beehive-monitor-web-application)
+- [Table of Contents](#table-of-contents)
+- [Basic Information](#basic-information)
+ - [Installation](#installation)
+ - [Note](#note)
+ - [Configuration](#configuration)
+ - [Usage](#usage)
+- [Backend](#backend)
+ - [Overview](#overview)
+- [Config](#config)
+ - [Overview](#overview-1)
+ - [`db.js`](#dbjs)
+- [Server](#server)
+ - [Overview](#overview-2)
+ - [`server.js`](#serverjs)
+- [Middleware](#middleware)
+ - [Overview](#overview-3)
+ - [`auth.middleware.js`](#authmiddlewarejs)
+ - [`errorHandler.middleware.js`](#errorhandlermiddlewarejs)
+- [Models](#models)
+ - [Overview](#overview-4)
+ - [`user.model.js`](#usermodeljs)
+ - [`apiary.model.js`](#apiarymodeljs)
+ - [`data.model.js`](#datamodeljs)
+- [Routes](#routes)
+ - [Overview](#overview-5)
+ - [`user.routes.js`](#userroutesjs)
+ - [`apiary.routes`](#apiaryroutes)
+ - [`data.routes`](#dataroutes)
+- [Controllers](#controllers-1)
+ - [`apiary.controller`](#apiarycontroller)
+ - [`data.controller`](#datacontroller)
+- [Frontend](#frontend)
+ - [Overview](#overview-6)
+- [`App.js`](#appjs)
+- [`index.js`](#indexjs)
+- [`index.css`](#indexcss)
+- [`theme.js`](#themejs)
+- [`store.js`](#storejs)
+- [Components](#components)
+ - [`AboutCard` React Component](#aboutcard-react-component)
+ - [`AddApiaryCard` React Component](#addapiarycard-react-component)
+ - [`AddDeviceCard` React Component](#adddevicecard-react-component)
+ - [`AddUserCard` React Component](#addusercard--react-component)
+ - [`ApiaryCard` React Component](#apiarycard-react-component)
+ - [`AutocompleteMaps` Typescript Component](#autocompletemaps-typescript-component)
+ - [`CustomTooltip` React Component](#customtooltip-react-component)
+ - [`DeviceCard` React Component](#-devicecard-react-component)
+ - [`FAQCard` React Component](#faqcard-react-component)
+ - [`Graph` React Component](#graph-react-component)
+ - [`Loading` React Component](#loading-react-component)
+ - [`Overview` React Component](#overview-react-component)
+ - [`SelectApiary` React Component](#selectapiary-react-component)
+ - [`Sidebar` React Component](#sidebar-react-component)
+ - [`Topbar` React Component](#topbar-react-component)
+ - [`UserCard` React Component](#usercard-react-component)
+- [Features](#features)
+ - [Apiary](#apiary)
+ - [`apiary.slice`](#apiaryslice)
+ - [`apiary.service`](#apiaryservice)
+ - [Auth](#auth)
+ - [`auth.slice`](#authslice)
+ - [`auth.service`](#authservice)
+- [Pages](#pages)
+ - [`About` React Page](#about-react-page)
+ - [`Dasboard` React Page](#dashboard-react-page)
+ - [`FAQ` React Page](#faq-react-page)
+ - [`Login` React Page](#login-react-page)
+ - [`Manage` React Page](#manage-react-page)
+ - [`Register` React Page](#register-react-page)
+- [Deployment](#deployment)
+- [Testing](#testing)
+- [License](#license)
+- [Acknowledgments](#acknowledgments)
+- [Appendix](#appendix)
+- [Screenshots](#screenshots)
+ - [Sign Up Page](#sign-up-page)
+ - [Sign In Page](#sign-in-page)
+ - [Dashboard Page](#dashboard-page)
+ - [Manage Page](#manage-page)
+ - [About Page](#about-page)
+ - [FAQ Page](#faq-page)
+- [Video Demo](#video-demo)
+- [Contact](#contact)
+
+Table of contents generated with markdown-toc
+
+# Basic Information
+
+## Installation
+
+To install the necessary dependencies for the frontend and backend, run the following command in the main directory:
+
+```bash
+npm run yayaya
+```
+
+To install the necessary dependencies for the backend only, run the following command in the main directory:
+
+```bash
+npm install
+```
+
+To install the necessary dependencies for the frontend only, run the following command in the `./frontend` directory:
+
+```bash
+npm install
+```
+
+### Note
+
+If you run into funding errors, try adding `--force` to the end of your command, and/or running the installations separately.
+
+## Configuration
+
+Use the template `.env` file to setup necessary configurations such as the MongoDB URI, JWT secret, Google Maps Locations API, and port. For those continuing this project, please contact cpaiz@scu.edu or paizcollin@gmail.com for details or assistance.
+
+## Usage
+
+To start the frontend and backend, run the following command in the main directory:
+
+```bash
+npm run dev
+```
+
+To start the backend only, run the following command in the main directory:
+
+```bash
+npm run server
+```
+
+To start the frontend only, run the following command in the main directory:
+
+```bash
+npm run client
+```
+
+The frontend will run on port 3000, and the backend will run on port 5000., by default.
+
+Using the Web Application is simple. The user will be greeted with a login page. If the user has an account, they can login with their credentials. If the user does not have an account, they can register for one. Once logged in, the user will be greeted with a dashboard. The dashboard will display the user's beehives, and the user can use the dropdown to select an apiary and subsequent beehive device to view more information about that specific hive. The user can also add, delete, and update apiaries, each of which is a small organization that contains its own set of devices (hives) that can be added, deleted, and updated. Users may also be added to apiaries to help manage them; members of apiaries can be granted different privileges, with the creator of the apiary having full control of the apiary and its hives. The user can also view the FAQ page, which will display frequently asked questions about the application, as well as the about page, which will display information about the application and its creators. The user can also logout of the application.
+
+# Backend
+
+## Overview
+
+The backend is neatly organized into individual modules, each of which is responsible for a specific set of tasks. The modules are as follows: `config`, `server`, `middleware`, `models`, `routes`, and `server`. Each module is described in detail below.
+
+# Config
+
+## Overview
+
+The `config` module is responsible for setting up the _MongoDB_ connection using Mongoose and the `ATLAS_URI` provided by the _MongoDB_ database.
+
+## `db.js`
+
+### Description
+
+This file is responsible for connecting the application to the _MongoDB_ database using the `ATLAS_URI` provided by the _MongoDB_ database. The `ATLAS_URI` is stored in the `.env` file, which is not included in the repository for security reasons. The `ATLAS_URI` is a string that contains the username, password, and database name for the _MongoDB_ database. The `db.js` file uses the `ATLAS_URI` to connect to the database using the `mongoose.connect()` method.
+
+### Usage Example
+
+```javascript
+const connectDB = require("./connectDB");
+
+// Call the function
+connectDB();
+```
+
+In this example, the `connectDB` function is imported from the `db.js` file and called. This will connect your application to the database using the `MongoDB URI` provided in the `.env` file.
+
+# Server
+
+## Overview
+
+The `server` is responsible for setting up the server and connecting to the _MongoDB_ database. The server is also responsible for listening on the port specified in the `.env` file.
+
+## `server.js`
+
+### Description
+
+This _Node.js_ sets up an _HTTP_ server using the _Express_ framework. It exports a single function that starts the server and listens for incoming requests.
+
+### Usage Example
+
+```javascript
+const startServer = require("./server");
+
+// Call the function
+startServer();
+```
+
+In this example, the `startServer` function is imported from the `server.js` file and called. This will start the server and listen for incoming requests.
+
+# Middleware
+
+## Overview
+
+The `middleware` module is responsible for setting up the middleware for the application.
+
+## `auth.middleware.js`
+
+### Description
+
+This is a middleware function that adds authentication to a _Node.js/Express_ application. The `protect` function checks for a valid _JSON Web Token (JWT)_ in the `Authorization` header of incoming _HTTP_ requests. If a valid token is found, the function decodes the token and sets the authenticated user in the request object. If a valid token is not found, the function returns a **401 Unauthorized response**.
+
+### Usage
+
+The `protect` function can be used as a middleware function in any _Express_ route that requires authentication. Here's an example of how to use the `protect` middleware function in an _Express_ route:
+
+```javascript
+const express = require("express");
+const protect = require("../middleware/protect");
+
+const router = express.Router();
+
+router.get("/", protect, (req, res) => {
+ res.json({ user: req.user });
+});
+```
+
+In this example, the `protect` middleware function is used as the second argument in the `router.get()` method. This means that the `protect` function will be called before the route handler function. If the _JWT_ is valid and the user is found, the `req.user` property will be set to the user object without the password field, and the route handler function will be called. If the _JWT_ is not valid or not provided, a **401 Unauthorized response** will be returned.
+
+### Function Parameters
+
+| Parameter | Description |
+| --------- | --------------------------------------------------------------------------------------------- |
+| `req` | The incoming HTTP request object. |
+| `res` | The outgoing HTTP response object. |
+| `next` | A function that passes control to the next middleware function in the request-response cycle. |
+
+### Function Flow
+
+The `protect` function follows this flow:
+
+1. Check if the `Authorization` header exists and starts with the word "Bearer".
+2. If the header exists and starts with "Bearer", try to verify the _JWT_.
+3. If the _JWT_ is valid, get the user ID from the token payload and find the user in the database.
+4. If the user is found, set the `req.user` property to the user object without the password field, and call the `next()` function to pass control to the next middleware.
+5. If the _JWT_ is not valid, return a **401 Unauthorized response** with an error message.
+6. If the `Authorization` header does not exist or does not start with "Bearer", return a **401 Unauthorized response** with an error message.
+
+## `errorHandler.middleware.js`
+
+### Description
+
+This is a middleware function that adds error handling to a _Node.js/Express_ application. The `errorHandler` function catches any errors that occur in the application and sends an appropriate error response to the client.
+
+### Usage Example
+
+The `errorHandler` function should be used as the last middleware function in the middleware stack of an _Express_ application. Here's an example of how to use the `errorHandler` middleware function in an _Express_ application:
+
+```javascript
+const express = require("express");
+const errorHandler = require("../middleware/errorHandler");
+
+const app = express();
+
+// Set up middleware functions
+app.use(express.json());
+app.use(cors());
+
+// Set up routes
+app.get("/", (req, res) => {
+ res.send("Hello, world!");
+});
+
+// Set up error handler middleware
+app.use(errorHandler);
+
+// Start server
+app.listen(3000, () => {
+ console.log("Server started on port 3000");
+});
+```
+
+In this example, the `errorHandler` middleware function is added to the middleware stack using the `app.use()` method. This means that the `errorHandler` function will be called if any middleware or route handler functions before it throw an error. The `errorHandler` function sends an error response with the error message and stack trace (if in development mode) to the client.
+
+| Parameter | Description |
+| --------- | --------------------------------------------------------------------------------------------- |
+| `err` | The error object that was thrown by a previous middleware or route handler function. |
+| `req` | The incoming HTTP request object. |
+| `res` | The outgoing HTTP response object. |
+| `next` | A function that passes control to the next middleware function in the request-response cycle. |
+
+### Function Flow
+
+The `errorHandler` function follows this flow:
+
+1. Get the status code from the `res` object. If the status code is not set, default to 500 (Internal Server Error).
+2. Set the HTTP status code of the response to the status code obtained in the previous step.
+3. Send a JSON response to the client with the error message and stack trace (if in development mode).
+
+### Error Handling
+
+The `errorHandler` function is itself an error handling middleware function, and its purpose is to catch errors thrown by previous middleware or route handler functions. If an error is thrown by a previous function, the `errorHandler` function will catch the error and send an appropriate error response to the client. The error message is sent as the `message` property of the JSON response, and the stack trace (if in development mode) is sent as the `stack` property of the JSON response.
+
+# Models
+
+## Overview
+
+The three models used in the application are the `User`, `Apiary`, and `Data` models. The `User` model is used to store user data in the database, the `Apiary` model is used to store apiary data in the database. and the `Data` model is used to store data from the devices in the database.
+
+## `user.model.js`
+
+### Description
+
+This is a schema definition for the `user` model in a _Node.js/Express_ application using _Mongoose_. The `userSchema` defines the structure and validation rules for user documents that will be stored in a _MongoDB_ database.
+
+### `userSchema`
+
+| Field | Type | Required | Unique | Example Value | Description |
+| --------- | -------- | -------- | ------ | ---------------------- | ------------------------------------------------ |
+| name | `String` | Yes | No | "John Doe" | The user's name. |
+| email | `String` | Yes | Yes | "johndoe@example.com" | The user's email address. Must be unique. |
+| password | `String` | Yes | No | "$2a$10$V7X9I...jMx7" | The user's password. |
+| createdAt | `Date` | No | No | "2023-04-25T12:34:56Z" | The timestamp for when the user was created |
+| updatedAt | `Date` | No | No | "2023-04-25T12:34:56Z" | The timestamp for when the user was last updated |
+
+Note that the `createdAt` and `updatedAt` fields are of type `Date` and are automatically generated by Mongoose using the `timestamps` option set to `true` in the schema.
+
+### Example Document
+
+```json
+{
+ "_id": "6153db097a880f72a2d6c853",
+ "name": "John Doe",
+ "email": "johndoe@example.com",
+ "password": "$2a$10$YSiN2VK/OdtZ5N5c5S5S5OAmQ2IyHz.gxCGKdDw.Ggchc20OaG1Ia",
+ "createdAt": "2021-09-28T05:45:29.586Z",
+ "updatedAt": "2021-09-28T05:45:29.586Z"
+}
+```
+
+Note that the password field is encrypted using _bcrypt_, so the value shown is just an example of an encrypted password hash. The actual value would be a long string of random characters.
+
+## `apiary.model.js`
+
+### Description
+
+This is a schema definition for the `apiary.model` in a _Node.js/Express_ application using _Mongoose_. The `apiarySchema` defines the structure and validation rules for apiary documents that will be stored in a MongoDB database. This model consists of a `geoSchema` which defines a location, a `memberSchema` which defines a list of users, a `deviceSchema` which defines a list of devices, and an `apiarySchema` which defines an apiary.
+
+### `geoSchema`
+
+| Field | Type | Required | Unique | Example Value | Description |
+| ---------------- | ----------------- | -------- | ------ | ----------------------------- | --------------------------------------------------------------------------------------------------------------------- |
+| type | `String` | No | No | "Point" | The type of the location data. Default value is "Point". |
+| coordinates | Array of `Number` | No | No | `[-73.9857, 40.7484]` | An array of longitude and latitude coordinates in that order. Indexed as a 2dsphere to enable location-based queries. |
+| formattedAddress | `String` | No | No | "123 Main St, New York, NY" | The formatted address of the location. |
+| placeID | `String` | No | No | "ChIJd8BlQ2BZwokRAFUEcm_qrcA" | The place ID of the location. |
+
+### `memberSchema`
+
+| Field | Type | Required | Unique | Example Value | Description |
+| ----- | ---------- | -------- | ------ | -------------------------- | -------------------------------------------------------------------- |
+| user | `ObjectId` | Yes | No | "61579f9a53e7e8f2916d47b1" | References a user object associated with the member |
+| role | `String` | No | No | "USER" | The role of the member, must be one of "USER", "ADMIN", or "CREATOR" |
+
+### `deviceSchema`
+
+| Field | Type | Required | Unique | Example Value | Description |
+| --------- | ---------- | -------- | ------ | -------------------------- | ---------------------------------------------------- |
+| serial | `String` | Yes | Yes | "ABC123" | The serial number of the device |
+| name | `String` | Yes | No | "Device 1" | The name of the device |
+| remote | `String` | Yes | Yes | "https://remote.it/ABC123" | The remote.it URL of the device |
+| data | `ObjectId` | No | No | "6154353e7eb3aa3b1886d051" | The ID of the data object associated with the device |
+| createdAt | `Date` | No | No | "2022-01-01T00:00:00.000Z" | The date and time when the device was created |
+| updatedAt | `Date` | No | No | "2022-01-02T00:00:00.000Z" | The date and time when the device was last updated |
+
+### `apiarySchema`
+
+| Field | Type | Required | Unique | Example Value | Description |
+| --------- | --------------------------------------- | -------- | ------ | ------------------ | ---------------------------------------------- |
+| name | String | No | No | "My Apiary" | The name of the Apiary |
+| location | Object of type `geoSchema` | No | No | See `geoSchema` | The coordinates of the Apiary |
+| members | Array of Objects of type `memberSchema` | Yes | No | See `memberSchema` | An array of members associated with the Apiary |
+| devices | Array of Objects of type `deviceSchema` | No | No | See `deviceSchema` | An array of devices associated with the Apiary |
+| createdAt | Date | No | No | - | The date when the Apiary was created |
+| updatedAt | Date | No | No | - | The date when the Apiary was last updated |
+
+### Example Document
+
+```json
+{
+ "_id": "61701a1d45c5556f37e18f22",
+ "name": "My Apiary",
+ "location": {
+ "type": "Point",
+ "coordinates": [50.1234, -120.5678],
+ "formattedAddress": "123 Main Street, Vancouver, BC, Canada",
+ "placeID": "ChIJ2-3w3EXyhlQRugc5uVrXQ8o"
+ },
+ "members": [
+ {
+ "_id": "61701a1d45c5556f37e18f23",
+ "user": "616f5c5dc5be5e8b5fabc123",
+ "role": "ADMIN"
+ },
+ {
+ "_id": "61701a1d45c5556f37e18f24",
+ "user": "616f5c5dc5be5e8b5fabc456",
+ "role": "USER"
+ }
+ ],
+ "devices": [
+ {
+ "_id": "61701a1d45c5556f37e18f25",
+ "serial": "ABC123",
+ "name": "Sensor 1",
+ "remote": "https://example.com/ABC123",
+ "data": "61701a1d45c5556f37e18f26",
+ "createdAt": "2022-10-19T12:34:56.789Z",
+ "updatedAt": "2022-10-19T12:34:56.789Z"
+ },
+ {
+ "_id": "61701a1d45c5556f37e18f27",
+ "serial": "DEF456",
+ "name": "Sensor 2",
+ "remote": "https://example.com/DEF456",
+ "data": null,
+ "createdAt": "2022-10-19T12:34:56.789Z",
+ "updatedAt": "2022-10-19T12:34:56.789Z"
+ }
+ ],
+ "createdAt": "2022-10-19T12:34:56.789Z",
+ "updatedAt": "2022-10-19T12:34:56.789Z"
+}
+```
+
+## `data.model.js`
+
+### Description
+
+This is a schema definition for a `data.model` in a _Node.js/Express_ application using _Mongoose_. The `dataSchema` defines the structure and validation rules for data documents that will be stored in a _MongoDB_ database. This model consists of a `dataPointSchema` which defines the structure of an individual data point, and a `dataSchema` which defines a list of datapoints.
+
+### `dataPointSchema`
+
+The `dataPointSchema` defines the schema for an individual data point. It consists of the following fields:
+
+| Field | Type | Required | Unique | Example Value | Description |
+| ------------------------- | -------- | -------- | ------ | ------------- | ---------------------------------------------------------------------------------- |
+| time | `Date` | No | No | Date.now | The time the data point was recorded |
+| raw_activity.x | `Number` | Yes | No | 5.2 | The x coordinate of the raw activity data |
+| raw_activity.y | `Number` | Yes | No | 3.8 | The y coordinate of the raw activity data |
+| weather.temp | `Number` | Yes | No | 25.4 | The temperature in Celsius at the time of recording |
+| weather.humidity | `Number` | Yes | No | 70.2 | The humidity in percent at the time of recording |
+| weather.windspeed | `Number` | Yes | No | 10.3 | The wind speed in meters per second at the time of recording |
+| prediction_activity.x | `Number` | Yes | No | 2.1 | The x coordinate of the predicted activity data |
+| prediction_activity.y | `Number` | Yes | No | 1.5 | The y coordinate of the predicted activity data |
+| last_prediction_deviation | `Number` | No | No | 0.7 | The deviation between the raw and predicted activity data at the time of recording |
+
+### `dataSchema`
+
+The `dataSchema` defines the schema for a collection of data points. It consists of the following fields:
+
+| Field | Type | Required | Unique | Example Value | Description |
+| ---------- | -------------------------- | -------- | ------ | -------------------------- | --------------------------------------------------- |
+| apiary | `ObjectId` | No | No | "6154353e7eb3aa3b1886d051" | Reference to the Apiary model |
+| serial | `String` | Yes | Yes | "ABC123" | Unique identifier of the device sending the data |
+| datapoints | Array of `dataPointSchema` | No | No | See `dataPointSchema` | Array containing the data points sent by the device |
+
+### Example document
+
+```json
+{
+ "_id": "60934c7a2a58581ed82b8d96",
+ "apiary": "6051902568f4a4dbd4f1c0e5",
+ "serial": "XYZ123",
+ "datapoints": [
+ {
+ "time": "2022-04-26T12:00:00.000Z",
+ "raw_activity": { "x": 10, "y": 20 },
+ "weather": { "temp": 25, "humidity": 80, "windspeed": 10 },
+ "prediction_activity": { "x": 12, "y": 22 },
+ "last_prediction_deviation": 2.5
+ },
+ {
+ "time": "2022-04-26T12:05:00.000Z",
+ "raw_activity": { "x": 11, "y": 21 },
+ "weather": { "temp": 26, "humidity": 81, "windspeed": 11 },
+ "prediction_activity": { "x": 13, "y": 23 },
+ "last_prediction_deviation": 2.0
+ }
+ ],
+ "createdAt": "2021-05-06T00:00:00.000Z",
+ "updatedAt": "2021-05-06T00:00:00.000Z"
+}
+```
+
+# Routes
+
+## Overview
+
+The `routes` module contains the route definitions for the _Node.js/Express_ application. The routes are organized into separate files based on their purpose. The `user.routes` file contains the routes for user authentication and authorization, the `apiary.routes` file contains the routes for the apiary endpoints, and the `data.routes` file contains the routes for the data endpoints.
+
+## `user.routes.js`
+
+### Description
+
+This file defines the routes for user authentication and authorization. The routes are protected by authentication middleware to ensure that only authenticated users can access them.
+
+### Route Endpoints
+
+| Endpoint | Request Type | Authentication | Description |
+| --------------------- | ------------ | -------------- | -------------------------------------------------- |
+| `/api/users/register` | `POST` | - | Registers a new user with the provided credentials |
+| `/api/users/login` | `POST` | - | Logs in a user with the provided credentials |
+| `/api/users/me` | `GET` | Protect | Gets the current user |
+
+## `apiary.routes`
+
+### Description
+
+This file defines the routes for the apiary related endpoints. The routes are protected by authentication middleware to ensure that only authenticated users can access them.
+
+### Route Endpoints
+
+| Endpoint | Request Type | Authentication | Description |
+| ------------------------------------------------------------------------------- | ------------ | -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| `/api/apiaries` | `GET` | `Protect` | Returns all apiaries associated with the authenticated user. (no data) |
+| `/api/apiaries/filter/:filterid` | `GET` | `Protect` | Returns all apiaries associated with the authenticated user. (includes device data filtered by time range; limited to 1000 datapoints for request efficiency) |
+| `/api/apiaries` | `POST` | `Protect` | Creates a new apiary associated with the authenticated user. |
+| `/api/apiaries/apiary/:apiary_id` | `PUT` | `Protect` | Updates the specified apiary associated with the authenticated user. |
+| `/api/apiaries/apiary/:apiary_id` | `DELETE` | `Protect` | Deletes the specified apiary associated with the authenticated user. |
+| `/api/apiaries/apiary/:apiary_id/setdevice` | `PUT` | `Protect` | Sets a new device for the specified apiary associated with the authenticated user. |
+| `/api/apiaries/apiary/:apiary_id/device/:device_id/updatedevice` | `PUT` | `Protect` | Updates the specified device for the specified apiary associated with the authenticated user. |
+| `/api/apiaries/apiary/:apiary_id/device/:device_id/serial/:serial/deletedevice` | `PUT` | `Protect` | Deletes the specified device for the specified apiary associated with the authenticated user. |
+| `/api/apiaries/apiary/:apiary_id/setmember` | `PUT` | `Protect` | Sets a new member for the specified apiary associated with the authenticated user. |
+| `/api/apiaries/apiary/:apiary_id/user/:user_id/updatemember` | `PUT` | `Protect` | Updates the specified member for the specified apiary associated with the authenticated user. |
+| `/api/apiaries/apiary/:apiary_id/user/:user_id/deletemember` | `PUT` | `Protect` | Deletes the specified member for the specified apiary associated with the authenticated user. |
+
+## `data.routes`
+
+### Description
+
+This file defines the routes for the data related endpoints.
+
+### Controllers
+
+The `controllers` directory contains the functions that handle the logic for each route.
+
+### Route Endpoints
+
+| Endpoint | Request Type | Authentication | Description |
+| -------------------------- | ------------ | -------------- | --------------------------------------------------------------------------------- |
+| `/api/data/serial/:serial` | `PUT` | None | Puts the data from the device with the specified serial number into the database. |
+
+# Controllers
+
+## `user.controller`
+
+This file handles user-related routes and requests.
+
+### Functions
+
+| Function | Route | Access | Description |
+| ------------- | ----------------------- | ------- | -------------------------------------------------------------------------------- |
+| registerUser | `POST /api/users` | Public | Registers a new user. |
+| loginUser | `POST /api/users/login` | Public | Authenticates a user and generates a _JSON Web Token_ (_JWT_) for authorization. |
+| getMe | `GET /api/users/me` | Private | Gets user data based on the ID of the authenticated user. |
+| generateToken | - | - | Generates a _JSON Web Token_ (_JWT_) for authorization. |
+
+## `apiary.controller`
+
+### Description
+
+This file handles apiary-related routes and requests, including creating, updating, and deleting apiaries; setting, updating, and deleting devices; and setting, updating, and deleting members.
+
+### Functions
+
+Here is the updated table with the new GET request:
+
+| Function | Route | Access | Description |
+| ----------------------- | ----------------------------------------------------------------------------------- | ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------ |
+| checkUserToApiary | - | Private; apiary admins | Checks if the logged-in user is a member of the specified apiary and has admin privileges. |
+| getApiaries | `GET /api/apiaries` | Private; all users | Retrieves all apiaries associated with the currently logged-in user. |
+| getApiaryWithDeviceData | `GET /api/apiaries/filter/:filter` | Private; all users | Retrieves apiaries with device data based on the specified date filter. Data is limited to 1000 datapoints for speed and efficiency. |
+| setApiary | `POST /api/apiaries` | Private; all users | Creates a new apiary. |
+| updateApiary | `PUT /api/apiaries/apiary/:apiary_id` | Private; apiary admins | Updates the specified apiary's name and location. |
+| deleteApiary | `DELETE /api/apiaries/apiary/:apiary_id` | Private; apiary creator | Deletes the specified apiary and all associated data. |
+| setDevice | `PUT /api/apiaries/apiary/:apiary_id/setdevice` | Private; apiary admins | Sets a new device to an apiary. |
+| updateDevice | `PUT /api/apiaries/apiary/:apiary_id/device/:device_id/updatedevice` | Private; apiary admins | Updates an existing device in an apiary. |
+| deleteDevice | `PUT /api/apiaries/apiary/:apiary_id/device/:device_id/serial/:serial/deletedevice` | Private; apiary admins | Deletes an existing device from an apiary. |
+| setMember | `PUT /api/apiaries/apiary/:apiary_id/setmember` | Private; apiary admins | Updates the user's role in the apiary to Editor. |
+| updateMember | `PUT /api/apiaries/apiary/:apiary_id/user/:user_id/updatemember` | Private; apiary admins | Updates the user's role in the apiary to Owner. |
+| deleteMember | `PUT /api/apiaries/apiary/:apiary_id/user/:user_id/deletemember` | Private; apiary admins | Deletes the specified user from the apiary. |
+
+## `data.controller`
+
+### Description
+
+This file handles data-related routes and requests. It includes uploading data points to the database.
+
+### Functions
+
+| Function | Route | Access | Description |
+| -------- | ------------------------------- | ------------------------------------------ | ------------------------------------------------------------------------- |
+| putData | `POST /api/data/serial/:serial` | Needs protection (ML team authorized only) | Uploads a data point to the database based on the provided serial number. |
+
+# Frontend
+
+## Overview
+
+The frontend of the application is built using _React_. The `src` directory contains the source code for the frontend, including the components, pages, stylsheets, and features.
+
+# `App.js`
+
+## Description
+
+The `App.js` component is the root component of the application. It is responsible for rendering the application's navigation bar and routing the user to the appropriate page based on the URL.
+
+# `index.js`
+
+## Description
+
+The `index` file is the entry point of the application. It renders the `App` component and wraps it in a `BrowserRouter` component to enable routing.
+
+# `index.css`
+
+## Description
+
+The `index.css` file contains the global styles for the application.
+
+## Fonts
+
+This stylesheet imports the "Poppins" font family from Google Fonts with three weights - 400 (regular), 600 (semi-bold), and 700 (bold) - and the display property set to "swap".
+
+```css
+@import url("https://fonts.googleapis.com/css2?family=Poppins:wght@400;600;700&display=swap");
+```
+
+## Height, Width and Font
+
+The stylesheet applies 100% height and width to the `html`, `body`, `#root`, `.app`, and `.content` selectors, and uses the "Poppins" font family as the default font for these elements.
+
+```css
+html,
+body,
+#root,
+.app,
+.content {
+ height: 100%;
+ width: 100%;
+ font-family: "Poppins", sans-serif;
+}
+```
+
+## App Display and Position
+
+The `.app` selector has its display set to "flex" and position set to "relative".
+
+```css
+.app {
+ display: flex;
+ position: relative;
+}
+```
+
+## Scrollbar Styling
+
+The stylesheet applies custom styling to the scrollbar using the WebKit CSS scrollbar pseudo-elements. The width of the scrollbar is set to 10px, the track color is set to #e0e0e0, and the handle color is set to #888. On hover, the track color changes to #555.
+
+```css
+::-webkit-scrollbar {
+ width: 10px;
+}
+
+/* Track */
+::-webkit-scrollbar-track {
+ background: #e0e0e0;
+}
+
+/* Handle */
+::-webkit-scrollbar-thumb {
+ background: #888;
+}
+
+/* Handle on Hover */
+::-webkit-scrollbar-track:hover {
+ background: #555;
+}
+```
+
+## Fieldset Styling
+
+The `fieldset` selector has its border and outline set to "none" to remove any default styling applied to the element.
+
+```css
+fieldset {
+ border: none !important;
+ outline: none !important;
+}
+```
+
+That's it! This is a basic documentation of the CSS stylesheet provided.
+
+# `theme.js`
+
+## Description
+
+The `theme` file contains the color design tokens and MUI theme settings for the application. It exports two functions, `tokens` and `themeSettings`, which can be used to retrieve the color design tokens and MUI theme settings for the specified mode.
+
+## `tokens`
+
+The `tokens` function accepts a single argument, `mode`, which can either be `"light"` or `"dark"`, and returns an object containing the color design tokens for the specified mode.
+
+### Example Usage
+
+```jsx
+import { tokens } from "./theme";
+
+const darkModeTokens = tokens("dark");
+const lightModeTokens = tokens("light");
+```
+
+## `themeSettings`
+
+The `themeSettings` function accepts a single argument, `mode`, which can either be `"light"` or `"dark"`, and returns an object containing the MUI theme settings for the specified mode.
+
+### Example Usage
+
+```jsx
+import { createTheme } from "@mui/material/styles";
+import { themeSettings } from "./theme";
+
+const darkTheme = createTheme(themeSettings("dark"));
+const lightTheme = createTheme(themeSettings("light"));
+```
+
+# `store.js`
+
+## Description
+
+The `store` file configures a Redux store using `@reduxjs/toolkit`. It uses two slices, `auth` and `apiary`, to manage state related to user authentication and the application's apiary feature.
+
+# Components
+
+## `AboutCard` React Component
+
+### Description
+
+The `AboutCard` component is a reusable component that displays an information card with a title, avatar, and expandable content. It is used in the `About` page to display information regarding the project.
+
+### Props
+
+| Name | Type | Required | Description |
+| ---- | ------------------- | -------- | ---------------------------------------------------------------------------------------------------- |
+| faq | `List` of `Strings` | Yes | An array of two strings: the title of the card and the content to display when the card is expanded. |
+
+### State
+
+| Name | Type | Description |
+| ------ | --------- | ----------------------------------------------------- |
+| expand | `Boolean` | Determines whether the card is expanded or collapsed. |
+
+### Methods
+
+The `AboutCard` component does not define any methods.
+
+### Handlers
+
+The `AboutCard` component does not define any handlers.
+
+### Child Components
+
+| Name | Package | Description |
+| ------------------------- | --------------------- | --------------------------------------------------------------- |
+| ArrowDropDownOutlinedIcon | `@mui/icons-material` | An icon component that represents a downward-pointing arrow |
+| Avatar | `@mui/material` | A component that represents a user or entity |
+| Box | `@mui/material` | A component that provides a flexible container for content |
+| Card | `@mui/material` | A container component that displays information |
+| CardActions | `@mui/material` | A component that provides space for buttons or icons |
+| CardHeader | `@mui/material` | A component that displays a header for a card |
+| Collapse | `@mui/material` | A component that animates the expansion or collapse of content |
+| Grid | `@mui/material` | A layout component that helps to organize content in a grid |
+| IconButton | `@mui/material` | A button component that displays an icon |
+| InfoOutlinedIcon | `@mui/icons-material` | An icon component that represents an information icon |
+| Typography | `@mui/material` | A component that displays text content |
+| tokens | Custom function | A function that returns color tokens based on the current theme |
+| useTheme | `@mui/material` | A hook that provides access to the current theme |
+| useState | `react` | A hook that adds state to a functional component |
+
+### Usage Example
+
+```jsx
+import AboutCard from "./AboutCard";
+
+const ExampleComponent = () => {
+ const faq = ["Title", "Content"];
+
+ return ;
+};
+```
+
+In this example, we import the `AboutCard` component and render it with an array of two strings that will be displayed as the title and content of the card. When the user clicks on the arrow button in the card header, the content of the card will expand or collapse.
+
+## `AddApiaryCard` React Component
+
+### Description
+
+The `AddApiaryCard` component is a form card used to create a new apiary in a _React_ application. It includes a name input field, a location input field powered by the `GoogleMaps` component, and a button to submit the form data.
+
+### Props
+
+The `AddApiaryCard` component does not accept any props.
+
+### State
+
+| Name | Type | Description |
+| -------- | --------- | ---------------------------------------------------------------------------------- |
+| value | `Object` | The selected location value from the GoogleMaps component. |
+| expand | `Boolean` | Whether or not the card is expanded. |
+| formData | `Object` | The form data to be submitted, including the apiary name and location information. |
+
+### Methods
+
+The `AddApiaryCard` component does not define any methods.
+
+### Handlers
+
+| Name | Parameters | Description |
+| -------- | ------------ | -------------------------------------------------------------------- |
+| onChange | `e`: `Event` | Updates the form data when input fields are changed. |
+| onSubmit | `e`: `Event` | Submits the form data to the Redux store via the `setApiary` action. |
+
+### Child Components
+
+| Name | Package | Description |
+| --------------- | ----------------------------- | --------------------------------------------------------------------- |
+| AddOutlinedIcon | `@mui/icons-material` | A material design add icon. |
+| Autocomplete | `@mui/material` | A component to create an input field with autocomplete functionality. |
+| Avatar | `@mui/material` | A component to display user profile pictures or icons. |
+| Box | `@mui/material` | A layout component to create a box container. |
+| Button | `@mui/material` | A material design button component. |
+| Card | `@mui/material` | A material design card component. |
+| CardActions | `@mui/material` | A component to create a row of actions for a `Card` component. |
+| Collapse | `@mui/material` | A component to collapse content based on a boolean state variable. |
+| debounce | `lodash` | A function to debounce input field changes. |
+| GoogleMaps | `./AutocompleteMaps.tsx` | A custom component to display a Google Maps autocomplete input field. |
+| Grid | `@mui/material` | A layout component to create a grid of items. |
+| IconButton | `@mui/material` | A component to create clickable icons. |
+| parse | `autosuggest-highlight/parse` | A function to parse and highlight text in an autocomplete input. |
+| TextField | `@mui/material` | A material design input component. |
+| useTheme | `@mui/material` | A hook to access the current theme. |
+| useDispatch | `react-redux` | A hook to dispatch actions to the Redux store. |
+| useState | `react` | A hook to manage state variables in a functional component. |
+
+### Usage Example
+
+```jsx
+import AddApiaryCard from "./AddApiaryCard";
+
+const ParentComponent = () => {
+ return (
+
+ );
+};
+
+export default ParentComponent;
+```
+
+In this example, when the form is submitted, the data is sent to the Redux store via the `setApiary` action. This data includes the apiary name and location information provided by the `GoogleMaps` component.
+
+## `AddDeviceCard` React Component
+
+### Description
+
+This component renders a card with a form to add a new device to an apiary. The form includes an input field for the device name, a select field for the device type, and a button to submit the form data.
+
+### Props
+
+| Name | Type | Required | Description |
+| -------- | -------- | -------- | ------------------------------------------------------------ |
+| apiary | `Object` | Yes | An object with data for the apiary where the device is added |
+| userRole | `String` | Yes | A string with the user role, either "USER" or "ADMIN" |
+
+### State
+
+| Name | Type | Description |
+| -------- | --------- | -------------------------------------------------------- |
+| expand | `Boolean` | A boolean indicating whether the card is expanded or not |
+| formData | `Object` | An object containing the values of the form inputs |
+
+### Methods
+
+The `AddDeviceCard` component does not define any methods.
+
+### Handlers
+
+| Name | Parameters | Description |
+| -------- | ------------ | ---------------------------------------------------------- |
+| onChange | `e`: `Event` | A function to handle the onChange event of the form inputs |
+| onSubmit | `e`: `Event` | A function to handle the onSubmit event of the form |
+
+### Child Components
+
+| Name | Package | Description |
+| --------------- | --------------------- | ------------------------------------------------------------------------------------- |
+| AddOutlinedIcon | `@mui/icons-material` | An icon component that displays a plus symbol. |
+| Avatar | `@mui/material` | A component for displaying a circular image or icon. |
+| Box | `@mui/material` | A component for wrapping and styling its children. |
+| Button | `@mui/material` | A component for displaying a clickable button. |
+| Card | `@mui/material` | A container component for displaying content and actions related to a single subject. |
+| CardActions | `@mui/material` | A container component for grouping action buttons in a card. |
+| Collapse | `@mui/material` | A component that allows for collapsing and expanding its children. |
+| Grid | `@mui/material` | A layout component for arranging its children in a grid. |
+| IconButton | `@mui/material` | A button component that displays an icon. |
+| TextField | `@mui/material` | A component for displaying and inputting text. |
+| useDispatch | `react-redux` | A hook for dispatching actions to the Redux store. |
+| useSelector | `react-redux` | A hook for accessing state from the Redux store. |
+| useNavigate | `react-router-dom` | A hook for navigating to different routes in a React application. |
+| useTheme | `@mui/material` | A hook that provides access to the MUI theme. |
+| useState | `React` | A hook for managing state in functional components. |
+
+### Usage Example
+
+```jsx
+import React from "react";
+import AddDeviceCard from "./AddDeviceCard";
+
+const MyComponent = () => {
+ return (
+
+ );
+};
+
+export default MyComponent;
+```
+
+In this example, `MyComponent` renders the `AddDeviceCard` component passing the required props `apiary` and `userRole`. The `apiary` prop is an object with the data for the apiary where the device will be added, and the `userRole` prop is a string indicating the role of the user, either "USER" or "ADMIN".
+
+## `AddUserCard` React Component
+
+### Description
+
+The `AddUserCard` component is a React component that displays a card with a button to expand a form to add a new user to an apiary. It takes in two props: `apiary` and `userRole`.
+
+### Props
+
+| Name | Type | Required | Description |
+| -------- | -------- | -------- | ------------------------------------------- |
+| apiary | `Object` | Yes | An object containing data for the APIary. |
+| userRole | `String` | Yes | The role of the user creating the new user. |
+
+### State
+
+| Name | Type | Description |
+| -------- | --------- | ---------------------------------------------------------- |
+| expand | `Boolean` | Determines whether the form to add a new user is expanded. |
+| formData | `Object` | An object containing the email and role of the new user. |
+
+### Methods
+
+The `AddUserCard` component does not define any methods.
+
+### Handlers
+
+| Name | Parameters | Description |
+| -------- | ------------ | ---------------------------------------------------------------------------------------------------------------- |
+| onChange | `e`: `Event` | Updates the `formData` state with the input value of the form. |
+| onSubmit | `e`: `Event` | Dispatches an action to add a new member to the APIary with the email and role provided in the `formData` state. |
+
+### Child Components
+
+| Name | Package | Description |
+| ---------------- | --------------- | ------------------------------------------------------------------------------------------ |
+| Avatar | `@mui/material` | Displays an avatar with an icon. |
+| Box | `@mui/material` | A layout component that allows for flexible box sizing. |
+| Button | `@mui/material` | A button component with customizable styling. |
+| Card | `@mui/material` | A component that displays a card with a shadow effect. |
+| CardActions | `@mui/material` | A layout component that displays a set of buttons below the card content. |
+| Checkbox | `@mui/material` | A component that displays a checkbox. |
+| Collapse | `@mui/material` | A component that animates the expanding and collapsing of its children. |
+| FormControlLabel | `@mui/material` | A component that combines a label with a form control, such as a checkbox or radio button. |
+| Grid | `@mui/material` | A layout component that displays its children in a grid. |
+| IconButton | `@mui/material` | A button component that displays an icon. |
+| TextField | `@mui/material` | A component that displays an input field for text. |
+
+### Usage Example
+
+```jsx
+import AddUserCard from "./AddUserCard";
+
+const MyComponent = () => {
+ const apiary = { _id: "12345", name: "My APIary" };
+ const userRole = "ADMIN";
+
+ return ;
+};
+```
+
+In this example, the `MyComponent` component renders the `AddUserCard` component with an `apiary` object and a `userRole` string. The `AddUserCard` component displays a card with a button to expand a form to add a new user to the APIary.
+
+## `ApiaryCard` React Component
+
+### Description
+
+The `ApiaryCard` component is a reusable component in a _React_ application that displays information about an apiary such as its `name`, `location` and other details. It allows users to edit, delete and update the apiary information. This component utilizes _Material-UI_ library.
+
+### Props
+
+The following table lists the props that can be passed to `ApiaryCard` component:
+
+| Name | Type | Required | Description |
+| ------ | -------- | -------- | --------------------------------------- |
+| apiary | `Object` | Yes | An object containing apiary information |
+
+### State
+
+The `ApiaryCard` component has the following states:
+
+| Name | Type | Description |
+| -------- | --------- | ------------------------------------------------------------ |
+| expand | `Boolean` | Tracks whether the form for editing the apiary should expand |
+| formData | `Object` | Tracks the form data entered by the user for updating |
+
+### Methods
+
+The `ApiaryCard` component does not define any methods.
+
+### Handlers
+
+| Name | Parameters | Description |
+| -------- | ------------ | ----------------------------------------------- |
+| onChange | `e`: `Event` | Updates the formData state on form input change |
+| onSubmit | `e`: `Event` | Submits the form data to update the apiary |
+| onDelete | `e`: `Event` | Deletes the apiary from the application |
+
+### Child Components
+
+| Name | Package | Description |
+| --------------------- | --------------------- | ------------------------------------------------------------------------- |
+| AddDeviceCard | `./AddDeviceCard` | A form for adding a new device to the apiary. |
+| AddUserCard | `./AddUserCard` | A form for adding a new user to the apiary. |
+| Avatar | `@mui/material` | A component for displaying a user's avatar. |
+| Box | `@mui/material` | A layout component that displays child components in a box. |
+| Button | `@mui/material` | A component for displaying a button. |
+| Card | `@mui/material` | A component that displays a card. |
+| CardActions | `@mui/material` | A component for displaying actions in a card. |
+| CardHeader | `@mui/material` | A component for displaying a header in a card. |
+| Collapse | `@mui/material` | A component for animating the expansion and collapse of a card's content. |
+| DeviceCard | `./DeviceCard` | A card displaying details of a device in the apiary. |
+| DeviceHubOutlinedIcon | `@mui/icons-material` | An icon component representing a device hub. |
+| EditOutlinedIcon | `@mui/icons-material` | An icon component representing an edit button. |
+| Grid | `@mui/material` | A layout component that displays child components in a grid. |
+| GroupOutlinedIcon | `@mui/icons-material` | An icon component representing a group. |
+| IconButton | `@mui/material` | A component for displaying an icon that can be clicked. |
+| TextField | `@mui/material` | A component for displaying a text field that can be used for user input. |
+| Typography | `@mui/material` | A component for displaying text. |
+| useDispatch | `react-redux` | A hook that returns the Redux store's `dispatch` function. |
+| useState | `react` | A hook that allows functional components to use component-level state. |
+
+### Usage
+
+```jsx
+import { ApiaryCard } from "./components";
+
+const apiary = {
+ _id: 1,
+ name: "My Apiary",
+ location: {
+ latitude: 43.6532,
+ longitude: -79.3832,
+ formattedAddress: "Toronto, ON, Canada",
+ },
+ devices: [
+ {
+ _id: 1,
+ name: "Device 1",
+ macAddress: "00:00:00:00:00:01",
+ },
+ {
+ _id: 2,
+ name: "Device 2",
+ macAddress: "00:00:00:00:00:02",
+ },
+ ],
+ members: [
+ {
+ user: {
+ _id: 1,
+ name: "User 1",
+ email: "user1@example.com",
+ },
+ role: "CREATOR",
+ },
+ {
+ user: {
+ _id: 2,
+ name: "User 2",
+ email: "user2@example.com",
+ },
+ role: "USER",
+ },
+ ],
+};
+
+const App = () => {
+ return ;
+};
+```
+
+In this example, we will render an `ApiaryCard` component with the provided `apiary` object. Users can edit, delete and update the apiary details by expanding the form using the dropdown arrow button.
+
+## `AutocompleteMaps` Typescript Component
+
+### Description
+
+This component is a wrapper around the `Autocomplete` component from `MUI` that provides an autocomplete feature for locations using _Google Maps API_. The component suggests location results as the user types and selects a location from the options.
+
+### Props
+
+| Name | Type | Required | Description |
+| -------- | ----------- | -------- | ---------------------------------------------- |
+| value | `PlaceType` | Yes | The selected location value. |
+| setValue | `function` | Yes | A function to set the selected location value. |
+
+### State
+
+| Name | Type | Description |
+| ---------- | ---------------------- | --------------------------------------------------------------- |
+| inputValue | `String` | The current input value of the Autocomplete component. |
+| options | `readonly PlaceType[]` | An array of location options fetched from Google Maps API. |
+| loaded | `Boolean` | Indicates whether the Google Maps API script has loaded or not. |
+
+### Methods
+
+The `AutocompleteMaps` component does not define any methods.
+
+### Handlers
+
+The `AutocompleteMaps` component does not define any handlers.
+
+### Interfaces
+
+| Name | Type | Description |
+| ------------------------- | ----------- | -------------------------------------------------------------------------------- |
+| MainTextMatchedSubstrings | `interface` | An interface representing the matched substrings in the main text of a location. |
+| StructuredFormatting | `interface` | An interface representing the structured formatting of a location. |
+| PlaceType | `interface` | An interface representing a location. |
+
+### Child Components
+
+| Component | Package | Description |
+| -------------- | --------------------- | ----------------------------------------------------------------------------------------------------------------------------- |
+| Autocomplete | `@mui/material` | A component that provides an input field with an autocomplete dropdown, which displays a list of options based on user input. |
+| Box | `@mui/material` | A component used for creating layout containers that can contain other components. |
+| Grid | `@mui/material` | A component used for creating grid-based layouts. |
+| LocationOnIcon | `@mui/icons-material` | An icon component used for indicating a location. |
+| TextField | `@mui/material` | An input field component used for accepting user input. |
+| Typography | `@mui/material` | A component used for displaying text with different styles and variations. |
+
+### Usage Example
+
+```jsx
+import GoogleMaps from "./AutocompleteMaps.tsx";
+
+function MyComponent() {
+ const [value, setValue] = React.useState(null);
+
+ return ;
+}
+```
+
+In this example, the `value` prop is the currently selected place, and the `setValue` prop is a function that gets called with the selected place object when a new place is selected. You can pass these props to the `GoogleMaps` component and it will handle the rest.
+
+Note that you'll need to obtain a _Google Maps API_ key and enable the _Places API_ in order to use this component. You can do this by following the instructions in the [Google Maps JavaScript API documentation](https://developers.google.com/maps/gmp-get-started).
+
+## `CustomTooltip` React Component
+
+The `CustomTooltip` component is a custom tooltip component used for displaying additional information when hovering over a Rechart element. It receives `active`, `payload`, and `label` as props and renders the tooltip content accordingly, based on what metrics the Recharts graph is currently populated with.
+
+### Props
+
+| Name | Type | Required | Description |
+| ------- | --------- | -------- | ------------------------------------------------------------- |
+| active | `Boolean` | Yes | Determines whether the tooltip should be displayed or not. |
+| payload | `Array` | Yes | An array of data points representing the values of the chart. |
+| label | `String` | Yes | The label of the current data point being hovered. |
+
+### State
+
+The `CustomTooltip` component does not define any state.
+
+### Methods
+
+The `CustomTooltip` component does not define any methods.
+
+### Handlers
+
+The `CustomTooltip` component does not define any handlers.
+
+### Child Components
+
+| Name | Package | Description |
+| ---------- | --------------- | -------------------------------------- |
+| Box | `@mui/material` | A layout component for creating boxes. |
+| Typography | `@mui/material` | A component for displaying text. |
+
+### Usage Example
+
+```jsx
+import CustomTooltip from "./CustomTooltip";
+
+const Chart = () => {
+ // Chart component logic
+
+ return (
+
+ {/* Chart components */}
+
+ {/* Other components */}
+
+ );
+};
+
+export default Chart;
+```
+
+In this example, the `CustomTooltip` component is imported and rendered within a `Chart` component. The `CustomTooltip` component is given the `active`, `payload`, and `label` props, which represent the tooltip's visibility, data points, and label information, respectively. The `CustomTooltip` component will be displayed when `active` is `true` and there are valid `payload` and `label` values. This component can be used within a chart component to provide customized tooltip functionality.
+
+## `DeviceCard` React Component
+
+### Description
+
+The `DeviceCard` component is a _React_ component that displays information about a device in a card format. The card displays basic information about the device, including its name, serial number, and a remote link. Users with the appropriate access level can edit or delete the device using the provided buttons.
+
+### Props
+
+| Name | Type | Required | Description |
+| -------- | -------- | -------- | ----------------------------------------------------------------------------- |
+| device | `Object` | Yes | An object containing information about the device. |
+| apiary | `Object` | Yes | An object containing information about the apiary that the device belongs to. |
+| userRole | `String` | Yes | A string representing the user's role in the system. |
+
+### State
+
+| Name | Type | Description |
+| -------- | --------- | ------------------------------------------------------------------------------- |
+| expand | `Boolean` | A boolean value indicating whether the form for editing the device is expanded. |
+| formData | `Object` | An object containing the current form data for editing the device. |
+
+### Methods
+
+The `DeviceCard` component does not define any methods.
+
+### Handlers
+
+| Name | Parameters | Description |
+| -------- | ------------ | -------------------------------------------------------------------------- |
+| onChange | `e`: `Event` | A handler function to update the `formData` state when form fields change. |
+| onSubmit | `e`: `Event` | A handler function to submit the form for updating the device information. |
+| onDelete | `e`: `Event` | A handler function to delete the device. |
+
+### Child Components
+
+| Name | Package | Description |
+| ---------------- | --------------------- | ------------------------------------------------------------------------------------------------------------- |
+| Avatar | `@mui/material` | Displays a circular image or icon that represents a user or entity. |
+| Box | `@mui/material` | A container component that can be used to group and space out elements. |
+| Button | `@mui/material` | A component for user interaction, such as triggering an action or event. |
+| Card | `@mui/material` | A container component that is used to group related content. |
+| CardActions | `@mui/material` | A container component for buttons and other action elements that are placed after the main content of a card. |
+| CardHeader | `@mui/material` | A container component for displaying a header in a Card. |
+| Collapse | `@mui/material` | A component that enables a content to be shown or hidden based on its visibility. |
+| EditOutlinedIcon | `@mui/icons-material` | An icon that represents the edit action. |
+| Grid | `@mui/material` | A responsive grid container used for laying out and aligning elements in a grid system. |
+| IconButton | `@mui/material` | A clickable button that contains an icon. |
+| TextField | `@mui/material` | A component for getting user input from the keyboard. |
+| Typography | `@mui/material` | A component for displaying text. |
+| useDispatch | `react-redux` | A hook that returns a reference to the dispatch function that allows you to dispatch actions to the store. |
+| useState | `react` | A hook that adds state to functional components. |
+| useTheme | `@mui/material` | A hook that returns the current theme used by the application. |
+
+### Usage Example
+
+```jsx
+import React from "react";
+import { DeviceCard } from "./components";
+
+const MyComponent = () => {
+ const device = {
+ name: "Device 1",
+ serial: "123456789",
+ remote: "https://example.com/device1",
+ _id: "abc123",
+ };
+
+ const apiary = {
+ name: "My Apiary",
+ _id: "def456",
+ };
+
+ const userRole = "ADMIN";
+
+ return (
+
+
+
+ );
+};
+```
+
+In this example, we import the `DeviceCard` component and pass in an object with `device` information, an object with `apiary` information, and the current `userRole`. The component will render the device information in a card format, and the user can click the "Edit" button to expand the form for editing the device information or the "Delete" button to delete the device.
+
+## `FAQCard` React Component
+
+### Description
+
+The `FAQCard` component renders a card with an FAQ (Frequently Asked Questions) question and an expandable answer section. The answer section can be toggled by clicking on an arrow icon.
+
+### Props
+
+| Name | Type | Required | Description |
+| ---- | ----------------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------- |
+| faq | `List` of `List` of `Strings` | Yes | An array that contains the question and the answer of the FAQ item. The first element is the question and the second element is the answer. |
+
+### State
+
+| Name | Type | Description |
+| ------ | --------- | -------------------------------------------------------- |
+| expand | `Boolean` | Indicates whether the answer section is expanded or not. |
+
+### Methods
+
+The `FAQCard` component does not define any methods.
+
+### Handlers
+
+The `FAQCard` component does not define any event handlers.
+
+### Child Components
+
+| Name | Package | Description |
+| ----------- | --------------- | -------------------------------------------------------------- |
+| Avatar | `@mui/material` | An avatar component used for displaying an icon. |
+| Box | `@mui/material` | A component for grouping and organizing content. |
+| Card | `@mui/material` | A card component that displays the FAQ question and answer. |
+| CardActions | `@mui/material` | A component that holds the action buttons in the card header. |
+| CardHeader | `@mui/material` | A component that holds the card title and avatar. |
+| Collapse | `@mui/material` | A component that displays the answer section when expanded. |
+| Grid | `@mui/material` | A grid component used for layout. |
+| IconButton | `@mui/material` | An icon button component used for toggling the answer section. |
+| Typography | `@mui/material` | A component for displaying text. |
+| useTheme | `@mui/material` | A hook that provides access to the current theme object. |
+
+### Usage Example
+
+```jsx
+import FAQCard from "./components/FAQCard";
+
+const App = () => {
+ const faqList = [
+ [
+ "What is React?",
+ "React is a JavaScript library for building user interfaces.",
+ ],
+ [
+ "What is JSX?",
+ "JSX is a syntax extension for JavaScript that allows you to write HTML-like code in your JavaScript files.",
+ ],
+ ];
+
+ return (
+
+ {faqList.map((faq, index) => (
+
+ ))}
+
+ );
+};
+```
+
+In this example, we pass a list of FAQs to the `FAQCard` component as a prop. The component renders a `Card` for each FAQ, displaying the question as the title and an expand/collapse button. When the button is clicked, the answer is displayed in the `Collapse` component.
+
+## Graph Component
+
+The `Graph` component displays a _Recharts_ graph with data visualization. It utilizes the _Recharts_ library for rendering the chart and includes various customization options for visualizing different data points. The component receives props to determine the data to be displayed and allows toggling the visibility of different data series.
+
+### Props
+
+| Name | Type | Required | Description |
+| ----------------- | ---------- | -------- | -------------------------------------------------------------------- |
+| device | `Object` | Yes | The device object containing the data to be displayed on the graph. |
+| selectedFilter | `Object` | Yes | The selected filter options for the graph. |
+| setSelectedFilter | `Function` | Yes | A function to update the selected filter options. |
+| filterOptions | `Array` | Yes | An array of filter options to be displayed as buttons for the graph. |
+
+### State
+
+| Name | Type | Description |
+| -------------- | -------- | ------------------------------------------------------------------------------------------------ |
+| visible | `Object` | An object representing the visibility state of different data series on the graph. |
+| displayedDates | `Array` | An array of dates that have been displayed on the X-axis of the graph. Used for date formatting. |
+
+### Methods
+
+The `Graph` component does not define any methods.
+
+### Handlers
+
+| Name | Parameters | Description |
+| ---------------- | -------------- | ----------------------------------------------------- |
+| toggleVisibility | `e`: (`Event`) | Toggles the visibility of a data series on the graph. |
+
+### Child Components
+
+| Name | Package | Description |
+| ------------- | ----------------- | ----------------------------------------------------------------- |
+| CustomTooltip | `./CustomTooltip` | A custom tooltip component for displaying additional information. |
+
+### Usage Example
+
+```jsx
+import { Graph } from "./components/Graph";
+
+const App = () => {
+ const device = {
+ // ... device data object
+ };
+
+ const selectedFilter = {
+ // ... selected filter options object
+ };
+
+ const filterOptions = [
+ // ... array of filter options
+ ];
+
+ const setSelectedFilter = (filter) => {
+ // ... function to update selected filter options
+ };
+
+ return (
+
+ {/* ... */}
+
+ {/* ... */}
+
+ );
+};
+
+export default App;
+```
+
+In this example, the Graph component is imported and rendered within the App component. The necessary props, such as `device`, `selectedFilter`, `setSelectedFilter`, and `filterOptions`, are passed to the Graph component to determine the data and options for the graph.
+
+## `Loading` React Component
+
+### Description
+
+The `Loading` component displays a circular progress indicator from the Material-UI library.
+
+### Props
+
+The `Loading` component does not accept any props.
+
+### State
+
+The `Loading` component does not use any internal state.
+
+### Methods
+
+The `Loading` component does not define any methods.
+
+### Handlers
+
+The `Loading` component does not define any handlers.
+
+### Child Components
+
+The `Loading` component does not have any child components.
+
+### Usage Example
+
+```jsx
+import React from "react";
+import Loading from "./Loading";
+
+function MyComponent() {
+ return (
+
+
Loading Example
+
+
+ );
+}
+
+export default MyComponent;
+```
+
+In this example, the `Loading` component is used to display a loading spinner.
+
+## `Overview` ReactComponent
+
+The `Overview` component displays an overview of a selected device's information, such as its online status and activity within the last 24 hours. It receives the `device` prop to determine which device to display the overview for. Online status is determined by seeing if the device has sent a data point within the last 3 hours. The `Overview` component also displays the time duration that the device has been offline, if applicable.
+
+### Props
+
+| Name | Type | Required | Description |
+| ------ | -------- | -------- | ---------------------------------------------------- |
+| device | `Object` | Yes | The device object for which to display the overview. |
+
+### State
+
+| Name | Type | Description |
+| ----------- | --------- | ----------------------------------------------------------------------------- |
+| isOnline | `Boolean` | Represents whether the device is currently online. |
+| offlineTime | `Number` | The time duration in seconds that the device has been offline, if applicable. |
+
+### Methods
+
+| Name | Parameters | Description |
+| ---------------------- | ------------- | ----------------------------------------------------------------------------------------------- |
+| getAccumulatedActivity | `device` | Retrieves the accumulated activity of the device in the last 24 hours based on its data points. |
+| formatOfflineTime | `offlineTime` | Formats the offline time duration into a readable format (e.g., "2 days, 3 hours, 15 minutes"). |
+
+### Child Components
+
+| Name | Package | Description |
+| ------------------------------- | ------------------------------------------------- | --------------------------------------------------------------------------- |
+| Typography | `@mui/material` | A component for displaying text in various styles and variants. |
+| Avatar | `@mui/material` | A component for displaying images or icons representing a user or object. |
+| Box | `@mui/material` | A component for creating layout containers and boxes with custom styles. |
+| Card | `@mui/material` | A component for displaying cards with various content and styles. |
+| CardHeader | `@mui/material` | A component for displaying headers within a card with customizable content. |
+| Fade | `@mui/material` | A component for animating the entering and exiting of elements. |
+| Backdrop | `@mui/material` | A component that provides a backdrop overlay for other components. |
+| CheckCircleOutlineOutlinedIcon | `@mui/icons-material/CheckCircleOutlineOutlined` | An icon component for displaying a checkmark circle outline. |
+| CancelOutlinedIcon | `@mui/icons-material/CancelOutlined` | An icon component for displaying a cancel symbol. |
+| AddCircleOutlineOutlinedIcon | `@mui/icons-material/AddCircleOutlineOutlined` | An icon component for displaying a plus circle outline. |
+| RemoveCircleOutlineOutlinedIcon | `@mui/icons-material/RemoveCircleOutlineOutlined` | An icon component for displaying a minus circle outline. |
+| useSelector | `react-redux` | A React Redux hook for accessing the Redux store's state. |
+| Loading | Custom component | A custom component for displaying a loading animation. |
+
+### Usage Example
+
+```jsx
+import React, { useEffect, useState } from "react";
+import Overview from "./Overview";
+
+const Dashboard = () => {
+ const [selectedDevice, setSelectedDevice] = useState(null);
+
+ // Simulated data fetching and selection
+ useEffect(() => {
+ const fetchDevices = async () => {
+ // Fetch the list of devices from an API
+ const devices = await fetchDevicesFromAPI();
+
+ // Select the first device by default
+ if (devices.length > 0) {
+ setSelectedDevice(devices[0]);
+ }
+ };
+
+ fetchDevices();
+ }, []);
+
+ return (
+
+ {/* Render device selection UI */}
+
+
+ {/* Render Overview component */}
+
+
+ );
+};
+
+export default Dashboard;
+```
+
+In this example, the `Dashboard` component fetches a list of devices from an API and renders a UI for device selection. The `Overview` component is then rendered with the selected device passed as the `device` prop. The `Overview` component displays the overview information for the selected device, including its online status and activity within the last 24 hours.
+
+## `SelectApiary` React Component
+
+The `SelectApiary` component is used for selecting an `apiary` and `device`. It receives several props and renders two select dropdowns for choosing an `apiary` and `device`.
+
+### Props
+
+| Name | Type | Required | Description |
+| ----------------- | ---------- | -------- | ----------------------------------------------------- |
+| apiaries | `Array` | Yes | An array of apiaries from which the user can choose. |
+| apiary | `Object` | Yes | The currently selected apiary. |
+| device | `Object` | Yes | The currently selected device. |
+| setApiary | `Function` | Yes | A callback function for updating the selected apiary. |
+| setDevice | `Function` | Yes | A callback function for updating the selected device. |
+| setSelectedFilter | `Function` | Yes | A callback function for updating the selected filter. |
+| selectedFilter | `Object` | Yes | The currently selected filter. |
+| filterOptions | `Array` | Yes | An array of options for the filter dropdown. |
+
+### State
+
+The `SelectApiary` component does not have any internal state.
+
+### Methods
+
+| Name | Parameters | Description |
+| --------- | ------------ | ---------------------------------------------------------------- |
+| onChange | `e`: `Event` | A function called when the apiary select dropdown value changes. |
+| onChange2 | `e`: `Event` | A function called when the device select dropdown value changes. |
+
+### Handlers
+
+The `SelectApiary` component does not define any handlers.
+
+### Child Components
+
+| Name | Package | Description |
+| ----------- | --------------- | -------------------------------------------------------- |
+| Box | `@mui/material` | A layout component for creating boxes. |
+| useTheme | `@mui/material` | A hook for accessing the MUI theme. |
+| Select | `@mui/material` | A component for selecting options from a dropdown. |
+| FormControl | `@mui/material` | A component for wrapping form controls. |
+| InputLabel | `@mui/material` | A component for displaying labels for form controls. |
+| MenuItem | `@mui/material` | A component representing an item within a dropdown menu. |
+
+### Usage Example
+
+```jsx
+import SelectApiary from "./SelectApiary";
+
+const Dashboard = () => {
+ // Dashboard component logic
+
+ return (
+
+ {/* Other components */}
+
+ {/* Other components */}
+
+ );
+};
+
+export default Dashboard;
+```
+
+In this example, the `SelectApiary` component is imported and rendered within a `Dashboard` component. The `SelectApiary` component is provided with the necessary props to manage the selection of an apiary and device. This component can be used within a larger dashboard interface to allow users to select an apiary and device for further data visualization and analysis.
+
+## `Sidebar` React Component
+
+### Description
+
+The `Sidebar` component implements a sidebar for navigation. It uses the `react-pro-sidebar` and `@mui/material` packages. The sidebar consists of a menu with several items and submenus, and it displays information related to the user's apiaries and devices.
+
+### Props
+
+The `Sidebar` component does not accept any props.
+
+### State
+
+| Name | Type | Description |
+| ----------- | --------- | ------------------------------------------------------ |
+| isCollapsed | `Boolean` | Controls whether the sidebar is collapsed or expanded. |
+| selected | `String` | The name of the currently selected item. |
+
+### Methods
+
+The `Sidebar` component does not define any methods.
+
+### Handlers
+
+| Name | Parameters | Description |
+| -------------- | ---------- | -------------------------------- |
+| setIsCollapsed | `Boolean` | Updates the `isCollapsed` state. |
+| setSelected | `String` | Updates the `selected` state. |
+
+### Child Components
+
+| Name | Package | Description |
+| ------------------------- | ---------------------- | ---------------------------------------------------------------------------------------------------------------------- |
+| `Avatar` | `@mui/material` | A component that displays an avatar image. |
+| `Backdrop` | `@mui/material` | A component that provides a dark overlay behind the content to indicate that the content is disabled or not available. |
+| `Box` | `@mui/material` | A layout component that can be used to create a container with a specified width, height, padding, and margin. |
+| `createTheme` | `@mui/material/styles` | A function that creates a custom theme for the application. |
+| `HelpOutlineOutlinedIcon` | `@mui/icons-material` | An icon component that displays a help icon. |
+| `HomeOutlinedIcon` | `@mui/icons-material` | An icon component that displays a home icon. |
+| `HiveOutlinedIcon` | `@mui/icons-material` | An icon component that displays a hive icon. |
+| `IconButton` | `@mui/material` | A button component that displays an icon. |
+| `InfoOutlinedIcon` | `@mui/icons-material` | An icon component that displays an info icon. |
+| `Menu` | `react-pro-sidebar` | A component that renders a menu in the sidebar. |
+| `MenuItem` | `react-pro-sidebar` | A clickable menu item in the sidebar. |
+| `MenuOutlinedIcon` | `@mui/icons-material` | An icon component that displays a menu icon. |
+| `ProSidebar` | `react-pro-sidebar` | A sidebar component that provides a layout for the sidebar. It can be collapsed or expanded by the user. |
+| `SettingsOutlinedIcon` | `@mui/icons-material` | An icon component that displays a settings icon. |
+| `SidebarContent` | `react-pro-sidebar` | A component that displays the content of the sidebar. |
+| `SidebarFooter` | `react-pro-sidebar` | A component that displays a footer at the bottom of the sidebar. |
+| `SidebarHeader` | `react-pro-sidebar` | A component that displays a header at the top of the sidebar. |
+| `SubMenu` | `react-pro-sidebar` | A submenu that displays nested menus in the sidebar. |
+| `Typography` | `@mui/material` | A component that displays text. |
+| `useDispatch` | `react-redux` | A hook that returns a reference to the `dispatch` function of the Redux store. |
+| `useSelector` | `react-redux` | A hook that returns a selected value from the Redux store. |
+| `useTheme` | `@mui/material/styles` | A hook that returns the current theme of the application. |
+
+### Usage Example
+
+```jsx
+import { Sidebar } from "./components";
+
+const App = () => {
+ return (
+
+
+
+ );
+};
+```
+
+In this example, the `Sidebar` component is imported and used within a parent component. When rendered, it will display a sidebar with several menu items and submenus. The user's apiaries and devices will be shown in the corresponding submenus, which can be clicked to navigate to the corresponding pages. The sidebar can be collapsed and expanded by clicking on the button in the top left corner.
+
+## `Topbar` React Component
+
+### Description
+
+The `Topbar` component is a customizable top navigation bar that can be used in a _React_ application. It includes icons for toggling between light and dark mode, viewing notifications (no implementation), accessing settings, and logging out. The component can display the title of the page or section it represents.
+
+### Props
+
+| Name | Type | Required | Description |
+| ----- | -------- | -------- | ------------------------------------------- |
+| title | `String` | no | The title of the page or section to display |
+
+### State
+
+The `Topbar` component does not define any state.
+
+### Methods
+
+The `Topbar` component does not define any methods.
+
+### Handlers
+
+| Name | Parameters | Description |
+| --------- | ------------ | ----------------------------------------------------- |
+| onLogout | `e`: `Event` | Logs out the current user and redirects to login page |
+| colorMode | `e`: `Event` | Toggles the color mode between light and dark mode |
+
+### Child Components
+
+| Name | Package | Description |
+| ------------------------- | -------------------------------- | -------------------------------------------------------------------------------------------------------- |
+| Avatar | `@mui/material` | A component that displays a user's profile picture or initials. |
+| Box | `@mui/material` | A basic layout component that provides a flexible container for grouping and arranging other components. |
+| ColorModeContext | `../theme` | A context object that provides the current color mode and a method to toggle the color mode. |
+| DarkModeOutlinedIcon | `@mui/material` | An icon component that displays an outline of a light bulb for switching to dark mode. |
+| IconButton | `@mui/material` | A button component that displays an icon. |
+| LightModeOutlinedIcon | `@mui/material` | An icon component that displays an outline of a light bulb for switching to light mode. |
+| logout | `../features/auth/auth.slice.js` | An action creator that dispatches an action to log out the user. |
+| LogoutOutlinedIcon | `@mui/material` | An icon component that displays a door with an arrow icon for logging out. |
+| NotificationsOutlinedIcon | `@mui/material` | An icon component that displays a bell icon for showing notifications. |
+| reset | `../features/auth/auth.slice.js` | An action creator that dispatches an action to reset the auth state. |
+| useContext | `react` | A hook that provides access to a context object. |
+| useDispatch | `react-redux` | A hook that returns a reference to the dispatch function from the Redux store. |
+| useSelector | `react-redux` | A hook that returns selected parts of the state from the Redux store. |
+| SettingsOutlinedIcon | `@mui/material` | An icon component that displays a gear icon for opening the settings. |
+| Typography | `@mui/material` | A component that renders text in various styles and sizes. |
+| useNavigate | `react-router-dom` | A hook that returns a navigate function for programmatic navigation. |
+| useTheme | `@mui/material` | A hook that returns the current theme object. |
+
+### Usage
+
+```jsx
+import Topbar from "./Topbar";
+
+function MyPage() {
+ return (
+
+
+
This is the content of the page.
+
+ );
+}
+```
+
+In this example, the `Topbar` component is displayed at the top of the `MyPage` component with the title "My Page Title". The `p` tag below it represents the rest of the page content. The `Topbar` component can be customized with _CSS_ styling or _MUI_ theming.
+
+## `UserCard` React Component
+
+### Description
+
+The `UserCard` component is a card component that displays user information and allows for editing user roles and deleting users.
+
+### Props
+
+| Name | Type | Required | Description |
+| ------ | -------- | -------- | -------------------------------------- |
+| user | `Object` | Yes | The user object to be displayed. |
+| apiary | `Object` | Yes | The apiary object the user belongs to. |
+
+### State
+
+| Name | Type | Description |
+| -------- | --------- | ----------------------------------------------------------------------- |
+| expand | `Boolean` | A state variable that determines if the card should be expanded or not. |
+| formData | `Object` | A state variable that holds the user's data to be edited. |
+
+### Methods
+
+The `UserCard` component does not define any methods.
+
+### Handlers
+
+| Name | Parameters | Description |
+| -------- | ------------ | ------------------------------------------------------------------- |
+| onChange | `e`: `Event` | A function that handles form data changes. |
+| onSubmit | `e`: `Event` | A function that handles the form submission for updating user data. |
+| onDelete | `e`: `Event` | A function that handles the form submission for deleting a user. |
+
+### Child Components
+
+| Name | Package | Description |
+| ---------------- | --------------- | --------------------------------------------------------- |
+| Avatar | `@mui/material` | Displays the user's avatar. |
+| Box | `@mui/material` | Wraps the forms and aligns the editing menu. |
+| Button | `@mui/material` | Submits the form for saving changes or deleting the user. |
+| Card | `@mui/material` | Wraps the user's information. |
+| CardActions | `@mui/material` | Displays the edit icon to open the editing menu. |
+| CardHeader | `@mui/material` | Displays the user's name, email, and avatar. |
+| Checkbox | `@mui/material` | Allows for toggling between user roles. |
+| Collapse | `@mui/material` | Collapses the editing menu. |
+| FormControlLabel | `@mui/material` | Displays a label for the Checkbox. |
+| Grid | `@mui/material` | Wraps the edit icon for proper alignment. |
+| IconButton | `@mui/material` | The button component for the edit icon. |
+| Typography | `@mui/material` | Displays the user's name and email. |
+
+### Usage Example
+
+```jsx
+import UserCard from "./components/UserCard";
+
+const MyComponent = () => {
+ const apiary = {
+ _id: "1",
+ name: "My Apiary",
+ members: [
+ {
+ user: {
+ _id: "2",
+ name: "John Doe",
+ email: "johndoe@example.com",
+ },
+ role: "ADMIN",
+ },
+ {
+ user: {
+ _id: "3",
+ name: "Jane Smith",
+ email: "janesmith@example.com",
+ },
+ role: "USER",
+ },
+ ],
+ };
+
+ return (
+
+
+
+
+ );
+};
+```
+
+In this example, the `UserCard` component is used to display user information for two different members of the same apiary. The component is passed the user object and apiary object as props. When the edit icon is clicked, the editing menu is displayed, allowing the user's role to be changed or the user to be deleted.
+
+# Features
+
+## Apiary
+
+### `apiary.slice`
+
+#### Description
+
+This _Redux_ slice file manages the state related to Apiaries. The file contains a set of initial states, action creators and their respective reducer cases to handle the apiaries _CRUD_ operations. The file also integrates with the `apiaryService` module to perform the actual API calls.
+
+#### Initial States
+
+| Name | Type | Description |
+| --------- | --------- | ----------------------------------------------------------------------------- |
+| apiaries | `Array` | A list of objects representing the user's apiaries. |
+| isError | `Boolean` | Indicates if an error occurred while processing a request. |
+| isSuccess | `Boolean` | Indicates if a request was processed successfully. |
+| isLoading | `Boolean` | Indicates if a request is in progress. |
+| message | `String` | A message that provides additional information about the status of a request. |
+
+#### Actions
+
+| Name | Description |
+| ------------------------- | ------------------------------------------------------------------------------------------------------------------- |
+| getApiaries | This action creator dispatches an API call to retrieve a list of user apiaries without populating the devices' data |
+| getApiariesWithDeviceData | This action creator dispatches an API call to retrieve a list of user apiaries with populating the devices' data |
+| setApiary | This action creator dispatches an API call to create a new apiary for the user. |
+| updateApiary | This action creator dispatches an API call to update an existing apiary. |
+| deleteApiary | This action creator dispatches an API call to delete an existing apiary. |
+| setDevice | This action creator dispatches an API call to create a new device for an apiary. |
+| updateDevice | This action creator dispatches an API call to update an existing device of an apiary. |
+| deleteDevice | This action creator dispatches an API call to delete an existing device of an apiary. |
+| setMember | This action creator dispatches an API call to create a new member for an apiary. |
+| updateMember | This action creator dispatches an API call to update an existing member of an apiary. |
+
+#### Reducer Cases
+
+| Case | apiaries | isError | isSuccess | isLoading | message |
+| ----------------------------------- | ------------------------- | ------- | --------- | --------- | ----------------------------- |
+| getApiaries.pending | - | `false` | `false` | `true` | "Loading" |
+| getApiaries.fulfilled | Action payload (apiaries) | `false` | `true` | `false` | "Loaded successfully" |
+| getApiaries.rejected | - | `true` | `false` | `false` | "Error while loading" |
+| getApiariesWithDeviceData.pending | - | `false` | `false` | `true` | "Loading" |
+| getApiariesWithDeviceData.fulfilled | Action payload (apiaries) | `false` | `true` | `false` | "Loaded successfully" |
+| getApiariesWithDeviceData.rejected | - | `true` | `false` | `false` | "Error while loading" |
+| setApiary.pending | - | `false` | `false` | `true` | "Creating new apiary" |
+| setApiary.fulfilled | - | `false` | `true` | `false` | "Apiary created successfully" |
+| setApiary.rejected | - | `true` | `false` | `false` | "Error while creating apiary" |
+| updateApiary.pending | - | `false` | `false` | `true` | "Updating apiary" |
+| updateApiary.fulfilled | - | `false` | `true` | `false` | "Apiary updated successfully" |
+| updateApiary.rejected | - | `true` | `false` | `false` | "Error while updating apiary" |
+| deleteApiary.pending | - | `false` | `false` | `true` | "Deleting apiary" |
+| deleteApiary.fulfilled | - | `false` | `true` | `false` | "Apiary deleted successfully" |
+| deleteApiary.rejected | - | `true` | `false` | `false` | "Error while deleting apiary" |
+| setDevice.pending | - | `false` | `false` | `true` | "Creating new device" |
+| setDevice.fulfilled | - | `false` | `true` | `false` | "Device created successfully" |
+| setDevice.rejected | - | `true` | `false` | `false` | "Error while creating device" |
+| updateDevice.pending | - | `false` | `false` | `true` | "Updating device" |
+| updateDevice.fulfilled | - | `false` | `true` | `false` | "Device updated successfully" |
+| updateDevice.rejected | - | `true` | `false` | `false` | "Error while updating device" |
+| deleteDevice.pending | - | `false` | `false` | `true` | "Deleting device" |
+| deleteDevice.fulfilled | - | `false` | `true` | `false` | "Device deleted successfully" |
+| deleteDevice.rejected | - | `true` | `false` | `false` | "Error while deleting device" |
+
+### `apiary.service`
+
+This _Redux_ service file contains a set of functions that perform the actual API calls to the backend. The file also contains a set of functions that handle the response of the API calls and dispatch the appropriate actions to update the state of the `apiary.slice` file.
+
+#### Functions
+
+| Function | Parameters | Description |
+| ------------------------- | --------------------- | --------------------------------------------------------------------------------------------------------------------- |
+| getApiaries | `token` | Retrieves the list of Apiaries (without populating the devices' data) associated with the authenticated user's token. |
+| getApiariesWithDeviceData | `data`, `token` | Retrieves the list of Apiaries (with populating the devices' data) associated with the authenticated user's token. |
+| setApiary | `apiaryData`, `token` | Creates a new Apiary with the given data and token. |
+| updateApiary | `apiaryData`, `token` | Updates an existing Apiary with the given data and token. |
+| deleteApiary | `apiaryData`, `token` | Deletes an Apiary with the given data and token. |
+| setDevice | `apiaryData`, `token` | Sets a new device for an existing Apiary with the given data and token. |
+| updateDevice | `apiaryData`, `token` | Updates an existing device for an existing Apiary with the given data and token. |
+| deleteDevice | `apiaryData`, `token` | Deletes an existing device for an existing Apiary with the given data and token. |
+| setMember | `userData`, `token` | Sets a new member for an existing Apiary with the given data and token. |
+| updateMember | `userData`, `token` | Updates an existing member for an existing Apiary with the given data and token. |
+| deleteMember | `userData`, `token` | Deletes an existing member for an existing Apiary with the given data and token. |
+
+## Auth
+
+### `auth.slice`
+
+#### Description
+
+This _Redux_ slice file defines the `auth` slice for managing authentication-related state. It uses the `createSlice` and `createAsyncThunk` functions from the `@reduxjs/toolkit` package to define the initial state, actions, and reducer cases.
+
+#### Initial State
+
+| Name | Type | Description |
+| --------- | ------------------ | ----------------------------------------------------------------------- |
+| user | `Object` or `Null` | The currently logged in user, retrieved from local storage. |
+| isError | `Boolean` | Indicates whether an error occurred during an async operation. |
+| isSuccess | `Boolean` | Indicates whether an async operation was successful. |
+| isLoading | `Boolean` | Indicates whether an async operation is currently in progress. |
+| message | `String` | An error message or success message associated with an async operation. |
+
+#### Actions
+
+| Name | Description |
+| -------- | -------------------------------------------- |
+| register | Async action that registers a user. |
+| login | Async action that logs in a user. |
+| logout | Async action that logs out the current user. |
+| reset | Action that resets the authentication state. |
+
+#### Reducer Cases
+
+| Case | User | Error | Success | Loading | Message |
+| ------------------ | --------------------- | ------------------------------ | ------- | ------- | ------------------------------ |
+| register.pending | - | - | - | `true` | - |
+| register.fulfilled | Action payload (user) | - | `true` | `false` | - |
+| register.rejected | - | Action payload (error message) | - | `false` | Action payload (error message) |
+| login.pending | - | - | - | `true` | - |
+| login.fulfilled | Action payload (user) | - | `true` | `false` | - |
+| login.rejected | - | Action payload (error message) | - | `false` | Action payload (error message) |
+| logout.fulfilled | - | - | - | - | - |
+
+Note: In the reducer cases table, the `Action payload` column refers to the value returned by the corresponding async action.
+
+### `auth.service`
+
+#### Description
+
+This _Redux_ service file contains a set of functions that perform the actual API calls to the backend. The file also contains a set of functions that handle the response of the API calls and dispatch the appropriate actions to update the state of the `auth.slice` file.
+
+#### Functions
+
+| Function | Parameters | Description |
+| -------- | ---------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| register | `userData` | Registers a new user with the provided user data. The `userData` parameter should be an object that includes the user's email, username, and password. If the registration is successful, the user's information will be saved to local storage. Returns the response data from the server. |
+| login | `userData` | Authenticates a user with the provided user data. The `userData` parameter should be an object that includes the user's email and password. If the user has selected the "Remember Me" checkbox, their information will be saved to local storage. Returns the response data from the server. |
+| logout | - | Removes the user's information from local storage, effectively logging them out. |
+
+# Pages
+
+## `About` React Page
+
+### Description
+
+The `About` Page displays information about the project and the team behind it.
+
+### Props
+
+The `About` component does not receive any props.
+
+### State
+
+The `About` component does not have any state.
+
+### Methods
+
+The `About` component does not define any methods.
+
+### Handlers
+
+The `About` component does not define any handlers.
+
+### Child Components
+
+| Component Name | Package | Description |
+| ---------------- | ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
+| AboutCard | `../components/AboutCard` | A custom component that displays a statement and its description in a card format. |
+| Avatar | `@mui/material` | A circular component that represents a user or object. It can display an image, icon, or text. |
+| Backdrop | `@mui/material` | A component that provides a dark overlay behind other components. It is often used to indicate that a process is running in the background. |
+| Box | `@mui/material` | A container component that can contain other MUI components or HTML elements. It provides various layout options such as flexbox and grid. |
+| Fade | `@mui/material` | A transition component that animates the appearance of an element by gradually fading it in or out. |
+| Grid | `@mui/material` | A responsive grid container that can hold other MUI components or HTML elements. It allows for customization of column and row spacing for different screen sizes. |
+| Grow | `@mui/material` | A transition component that animates the appearance of an element by gradually increasing its size. |
+| InfoOutlinedIcon | `@mui/material` | An MUI icon component that displays an "info" icon. |
+| tokens | `../theme` | An object that provides color values based on the current theme. It is used to customize the styling of non-MUI components. |
+| Typography | `@mui/material` | A component for displaying text. It supports various typography styles such as headings, body text, and captions. |
+| useDispatch | `react-redux` | A hook that provides access to the dispatch function of the Redux store. It can be used to dispatch actions to update the state. |
+| useEffect | `react` | A hook that runs a side effect after rendering. It is used to perform actions such as updating the state or making API requests. |
+| useNavigate | `react-router-dom` | A hook that provides access to the navigation object. It can be used to navigate to different pages in the application. |
+| useSelector | `react-redux` | A hook that provides access to the state of the Redux store. It can be used to retrieve data from the store. |
+| useTheme | `@mui/material` | A hook that provides access to the current theme object. It can be used to customize the styling of MUI components. |
+
+### Usage Example
+
+```jsx
+import About from "../pages/About";
+
+const App = () => {
+ return (
+
+ );
+};
+```
+
+In this example, the renders the `About` component which displays information about the project and the team behind it.
+
+## `Dashboard` React Page
+
+The `Dashboard` component represents a dashboard page that displays various data visualizations and overviews. It utilizes several child components and props to provide a comprehensive view of the user's data for each of their devices. Using the `SelectApiary` component, the user can select an `apiary` and a corresponding `device` to then populate the dashboard (Recharts graph and overview) with the data from that device. The `Dashboard` component also provides a filtering component that allows the user to filter the data by date range, as well as select which metrics to populate the graph with.
+
+### Props
+
+The `Dashboard` component does not receive any props.
+
+## State
+
+| Name | Type | Description |
+| -------------- | -------- | ---------------------------------- |
+| apiary | `String` | Stores the selected apiary. |
+| device | `String` | Stores the selected device. |
+| ovDevice | `String` | Stores the overview device. |
+| selectedFilter | `Object` | Stores the selected filter option. |
+
+## Methods
+
+The `Dashboard` component does not define any methods.
+
+## Handlers
+
+The `Dashboard` component does not define any handlers.
+
+## Child Components
+
+| Name | Package | Description |
+| ------------------------- | --------------------------------- | ----------------------------------------------------------- |
+| Box | `@mui/material` | A layout component for creating boxes. |
+| Typography | `@mui/material` | A component for displaying text. |
+| useTheme | `@mui/material` | A hook for accessing the theme object. |
+| Grid | `@mui/material` | A component for creating a responsive grid layout. |
+| useDispatch | `react-redux` | A hook for accessing the Redux dispatch function. |
+| useSelector | `react-redux` | A hook for accessing the Redux store state. |
+| useState | `react` | A hook for managing component state. |
+| useEffect | `react` | A hook for handling side effects in functional components. |
+| useNavigate | `react-router-dom` | A hook for accessing the navigation object in React Router. |
+| getApiariesWithDeviceData | `../features/apiary/apiary.slice` | A Redux action for fetching apiaries with device data. |
+| reset | `../features/apiary/apiary.slice` | A Redux action for resetting apiary data. |
+| toast | `react-toastify` | A function for displaying toast notifications. |
+| Graph | `../components/Graph` | A custom graph component. |
+| Overview | `../components/Overview` | A custom overview component. |
+| SelectApiary | `../components/SelectApiary` | A component for selecting an apiary. |
+
+## Usage Example
+
+```jsx
+import Dashboard from "./Dashboard";
+
+const App = () => {
+ return (
+
+ {/* Other components */}
+
+ {/* Other components */}
+
+ );
+};
+
+export default App;
+```
+
+In this example bove, the `Dashboard` component is imported and rendered within the `App` component. This allows the `Dashboard` to be displayed as a part of the larger application. Other components can be added before and after the `Dashboard` to create a complete user interface.
+
+## `FAQ` React Page
+
+### Description
+
+The `FAQ` page renders a frequently asked questions page. It displays a list of FAQ cards, each with a questions and their corresponding answer.
+
+### Props
+
+The `FAQ` page does not receive any props.
+
+### State
+
+The `FAQ` page does not have any state.
+
+### Methods
+
+The `FAQ` page does not define any methods.
+
+### Handlers
+
+The `FAQ` page does not define any handlers.
+
+### Child Components
+
+| Name | Package | Description |
+| ----------------------- | ----------------------- | -------------------------------------------------------------------------- |
+| Box | `@mui/material` | A container component used for grouping and spacing elements |
+| Grid | `@mui/material` | A responsive layout component used for aligning and distributing elements |
+| Typography | `@mui/material` | A component for displaying text with customizable typography |
+| Avatar | `@mui/material` | A component for displaying user avatars or icons |
+| useTheme | `@mui/material` | A hook that provides access to the MUI theme object |
+| HiveOutlinedIcon | `@mui/icons-material` | An icon component for displaying a hive outline |
+| tokens | `../theme` | A module that exports a function returning theme tokens |
+| Loading | `../components/Loading` | A component for displaying a loading animation |
+| useDispatch | `react-redux` | A hook that returns the Redux store's dispatch method |
+| useSelector | `react-redux` | A hook that returns selected state from the Redux store |
+| useEffect | `react` | A hook that allows performing side effects in function components |
+| useNavigate | `react-router-dom` | A hook that returns a navigate function to use for programmatic navigation |
+| HelpOutlineOutlinedIcon | `@mui/icons-material` | An icon component for displaying a help outline |
+| FAQCard | `../components/FAQCard` | A component that displays a question and answer card |
+| toast | `react-toastify` | A module for displaying toast notifications |
+| Grow | `@mui/material` | A component for animating element growth |
+| Fade | `@mui/material` | A component for animating element opacity |
+| Backdrop | `@mui/material` | A component for displaying a translucent background overlay |
+
+## `Login` React Page
+
+### Description
+
+The `Login` page allows users to log into an application. The component receives user inputs of `email`, `password`, and `isChecked`. It dispatches a login action with the input data to authenticate users via the `auth.slice.js` module. It also displays a loading spinner while the authentication is in progress. If the authentication fails, it displays an error message. The user is prompted to enter their email and password. The user can also select the `Remember me` checkbox to save their login credentials.
+
+### Props
+
+The `Login` component does not accept any props.
+
+### State
+
+| Name | Type | Description |
+| ---------- | --------- | --------------------------------------------------------------------------------------------------- |
+| `trans` | `Boolean` | A state variable that determines whether to transition to another view. |
+| `formData` | `Object` | A state variable that stores the user's input data, including `email`, `password`, and `isChecked`. |
+
+### Methods
+
+The `Login` page does not define any methods.
+
+### Handlers
+
+| Name | Parameters | Description |
+| ----------- | ------------ | ---------------------------------------------------------------------------------------------------- |
+| handleTrans | - | A function that toggles the `trans` state to transition to another view. |
+| onChange | `e`: `Event` | A function that updates the `formData` state with the new user input data. |
+| onSubmit | `e`: `Event` | A function that dispatches the login action with the `formData` state data to authenticate the user. |
+
+### Child Components
+
+| Name | Package | Description |
+| ------- | --------------- | ---------------------------------------------------------------- |
+| Loading | `../components` | A spinner that displays while the authentication is in progress. |
+
+### Usage Example
+
+```jsx
+import React from "react";
+import Login from "./Login";
+
+const App = () => {
+ return ;
+};
+
+export default App;
+```
+
+In this example, we import the `Login` component and render it in the `App` component. This will display the login form to the user.
+
+## `Manage` React Page
+
+### Description
+
+The `Manage` page allows users to manage their apiaries. It displays a list of existing apiaries that the user has access to, and allows the user to create new apiaries, edit existing apiaries, and delete apiaries. Within each apiary card, the user can view the apiary's devices/members, add new devices/members, edit existing devices/members, and delete devices/members.
+
+### Props
+
+The `Manage` component does not receive any props.
+
+### State
+
+The `Manage` component does not have any state.
+
+### Methods
+
+The `Manage` component does not define any methods.
+
+### Handlers
+
+The `Manage` component does not define any handlers.
+
+### Child Components
+
+| Name | Package | Description |
+| ------------- | ----------------------------- | ----------------------------------------------------------------------- |
+| Loading | `../components/Loading` | Displays a loading spinner while data is being fetched from the server. |
+| UserCard | `../components/UserCard` | Displays information about the user. |
+| DeviceCard | `../components/DeviceCard` | Displays information about a device. |
+| ApiaryCard | `../components/ApiaryCard` | Displays information about an apiary. |
+| AddApiaryCard | `../components/AddApiaryCard` | Displays a form for adding a new apiary. |
+
+### Usage
+
+```jsx
+import Manage from "./Manage";
+
+function MyComponent() {
+ return (
+
+
+
+ );
+}
+```
+
+In this example, the `Manage` component will display a list of existing apiaries, along with a form for adding a new apiary. If the user is not logged in, they will be redirected to the login page. If there is an error fetching data from the server, an error message will be displayed. If data is being fetched from the server, a loading spinner will be displayed.
+
+## `Register` React Page
+
+### Description
+
+The `Register` component is a form that allows users to register for a new account. This component is built using React and various components from the Material UI library. It relies on the Redux store to handle user authentication and makes use of React Router DOM to handle navigation. The user is prompted to enter their name, email, password, and confirm password.The user can then submit the form to register for a new account. If the registration is successful, the user is redirected to the dashboard. If the registration fails, an error message is displayed.
+
+### Props
+
+The `Register` component does not accept any props.
+
+### State
+
+| Name | Type | Description |
+| -------- | --------- | ------------------------------------------------------------------------------- |
+| formData | `Object` | An object containing the form data for registration |
+| trans | `Boolean` | A boolean value indicating whether the component should be displayed or hidden. |
+
+### Methods
+
+| Name | Parameters | Description |
+| -------- | ------------ | --------------------------------------------------------------------- |
+| onChange | `e`: `Event` | A callback function that updates the state with the new form data. |
+| onSubmit | `e`: `Event` | A callback function that submits the registration form to the server. |
+
+### Handlers
+
+The `Register` component does not define any handlers.
+
+### Child Components
+
+| Name | Package | Description |
+| ---------------- | --------------------- | ------------------------------------------------------------------------------------ |
+| Avatar | `@mui/material` | A Material UI component used to display an avatar icon. |
+| Backdrop | `@mui/material` | A Material UI component used to create a backdrop. |
+| Box | `@mui/material` | A Material UI component used to create a layout container. |
+| Button | `@mui/material` | A Material UI component used to render the "Sign Up" button. |
+| Container | `@mui/material` | A Material UI component used to create a responsive container. |
+| CssBaseline | `@mui/material` | A Material UI component used to reset the CSS styles to a consistent baseline. |
+| Fade | `@mui/material` | A Material UI component used to create a fade transition. |
+| FilledInput | `@mui/material` | A Material UI component used to create an input with filled background. |
+| Grid | `@mui/material` | A Material UI component used to create a responsive grid. |
+| Grow | `@mui/material` | A Material UI component used to create a grow transition. |
+| Link | `@mui/material` | A Material UI component used to render a link to the login page. |
+| LockOutlinedIcon | `@mui/icons-material` | A Material UI component used to display a lock icon. |
+| Loading | `../components` | A component that displays a loading spinner when the registration form is submitted. |
+| TextField | `@mui/material` | A Material UI component used to display and handle text input. |
+| ThemeProvider | `@mui/material` | A Material UI component used to provide a theme to the application. |
+| toast | `react-toastify` | A third-party library used to display toast notifications. |
+| Typography | `@mui/material` | A Material UI component used to display text. |
+
+### Usage
+
+```jsx
+import React from "react";
+import Register from "./components/Register";
+
+const App = () => {
+ return ;
+};
+
+export default App;
+```
+
+In this example, the `Register` component is imported and rendered in the main `App` component. When the user navigates to the "/register" route, the `Register` component will be displayed, allowing them to create a new account.
+
+# Deployment
+
+## Prerequisites
+
+- Heroku CLI
+
+## Instructions
+
+1. Install the Heroku CLI
+2. Login to Heroku
+3. Create a new Heroku app
+4. Set up the environment variables in the Heroku app
+5. Add the Heroku remote to the local git repository
+6. Push the local git repository to the Heroku remote
+
+## Troubleshooting
+
+If you encounter errors when deploying to Heroku, try running the following:
+
+1. Clear the Heroku build cache
+
+```bash
+$ heroku config:set NODE_MODULES_CACHE=false
+```
+
+2. Ensure the `scripts` section of the `package.json` file contains the following: (Note: the `--legacy-peer-deps` flag is required for Heroku to install the dependencies correctly)
+
+```json
+"scripts": {
+ "start": "node backend/server.js",
+ "server": "nodemon backend/server.js",
+ "client": "npm start --prefix frontend",
+ "dev": "concurrently \"npm run server\" \"npm run client\"",
+ "frontend": "npm install --prefix frontend",
+ "yayaya": "concurrently \"npm install\" \"npm run frontend\"",
+ "preinstall": "npx npm-force-resolutions",
+ "heroku-postbuild": "NPM_CONFIG_PRODUCTION=false npm install --prefix frontend --legacy-peer-deps && npm run build --prefix frontend --legacy-peer-deps"
+ },
+```
+
+# Testing
+
+Testing has been manual. Consider implementing a testing framework using Jest and Enzyme.
+
+# License
+
+Property of Santa Clara University under SCU's Senior Design Program
+
+# Acknowledgments
+
+Special thanks to Kian Nikzad, Wendy Mather, and Gerhard and Lisa Eschelbeck for their continued support on this project's creative direction and design decisions.
+
+# Appendix
+
+Include any additional information or resources, such as troubleshooting tips or frequently asked questions.
+
+# Screenshots
+
+## Sign Up Page
+
+
+
+## Sign In Page
+
+
+
+## Dashboard Page
+
+
+
+## Manage Page
+
+
+
+## About Page
+
+
+
+## FAQ Page
+
+
+
+# Video Demo
+
+[](https://www.youtube.com/watch?v=zT9ShMXCmIA)
+
+# Contact
+
+- Any questions regarding the web application, please contact **cpaiz@scu.edu** or **paizcollin@gmail.com**
+- Any questions regarding deployment, please contact **cpaiz@scu.edu** or **paizcollin@gmail.com** or **jstock@scu.edu**
+- Any questions regarding hardware, please contact **ewrysinski@scu.edu** and **dblanc@scu.edu**
diff --git a/backend/config/db.js b/backend/config/db.js
new file mode 100644
index 0000000..9214709
--- /dev/null
+++ b/backend/config/db.js
@@ -0,0 +1,15 @@
+const mongoose = require("mongoose");
+
+const connectDB = async () => {
+ try {
+
+ const conn = await mongoose.connect(process.env.ATLAS_URI);
+
+ console.log(`MongoDB Connected: ${conn.connection.host}`.cyan.underline);
+ } catch (error) {
+ console.log(error);
+ process.exit(1);
+ }
+};
+
+module.exports = connectDB;
diff --git a/backend/controllers/apiary.controller.js b/backend/controllers/apiary.controller.js
new file mode 100644
index 0000000..8657e42
--- /dev/null
+++ b/backend/controllers/apiary.controller.js
@@ -0,0 +1,594 @@
+const asyncHandler = require("express-async-handler");
+const Apiary = require("../models/apiary.model.js");
+const User = require("../models/user.model");
+const Data = require("../models/data.model");
+const mongoose = require("mongoose");
+
+// @status WORKING
+// @desc Check that user is part of apiary
+// @return Returns a user_id, role, and apiary
+async function checkUserToApiary(req, res) {
+ // Find apiary from param :apiary_id
+ const apiary = await Apiary.findById(req.params.apiary_id);
+
+ console.log(apiary);
+
+ // If apiary not found, error
+ if (!apiary) {
+ res.status(400);
+ throw new Error("Apiary not found");
+ }
+
+ // Check for user (logged in essentially, from protect)
+ if (!req.user) {
+ res.status(400);
+ throw new Error("User not found");
+ }
+
+ // Make sure the logged in user matches a member of this apiary and is an admin
+ var user;
+ var role;
+ apiary.members.forEach((member) => {
+ if (member.user.toString() === req.user.id) {
+ user = member.user;
+ role = member.role;
+ return;
+ }
+ });
+
+ return { user, role, apiary };
+}
+
+// @status WORKING
+// @desc Get apiaries (no device data)
+// @route GET /api/apiaries
+// @access Private; all users
+const getApiaries = asyncHandler(async (req, res) => {
+ // Find apiaries where current user (from protect) is a member
+ const apiaries = await Apiary.find({
+ members: {
+ $elemMatch: {
+ user: req.user.id,
+ },
+ },
+ }).populate("members.user");
+
+ res.status(200).json(apiaries);
+});
+
+// @status WORKING
+// @desc Get apiaries (with device data)
+// @route GET /api/apiaries/filter/:filter
+// @access Private; all users
+const getApiaryWithDeviceData = asyncHandler(async (req, res) => {
+ const filter = req.params.filter; // Assuming the filter parameter is passed as a query parameter
+
+ // console.log(filter);
+
+ let fromDate;
+ switch (filter) {
+ case "init":
+ fromDate = new Date(Date.now()); // Unix epoch (oldest date)
+ break;
+ case "1day":
+ fromDate = new Date(Date.now() - 24 * 60 * 60 * 1000); // One day ago
+ break;
+ case "1week":
+ fromDate = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000); // One week ago
+ break;
+ case "1month":
+ fromDate = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000); // One month ago
+ break;
+ case "3month":
+ fromDate = new Date(Date.now() - 90 * 24 * 60 * 60 * 1000); // Three months ago
+ break;
+ case "6month":
+ fromDate = new Date(Date.now() - 180 * 24 * 60 * 60 * 1000); // Six months ago
+ break;
+ case "1year":
+ fromDate = new Date(Date.now() - 365 * 24 * 60 * 60 * 1000); // One year ago
+ break;
+ case "2year":
+ fromDate = new Date(Date.now() - 730 * 24 * 60 * 60 * 1000); // Two years ago
+ break;
+ default:
+ // No filter or invalid filter, fetch all-time data
+ fromDate = new Date(0); // Unix epoch (oldest date)
+ break;
+ }
+
+ const apiaries = await Apiary.find({
+ members: { $elemMatch: { user: req.user.id } },
+ }).populate("members.user");
+
+ const updatedApiaries = await Promise.all(
+ apiaries.map(async (apiary) => {
+ const devices = apiary.devices;
+
+ const updatedDevices = await Promise.all(
+ devices.map(async (device) => {
+ const data = await Data.aggregate([
+ { $match: { _id: device.data } },
+ {
+ $project: {
+ _id: 0,
+ datapoints: {
+ $filter: {
+ input: "$datapoints",
+ as: "point",
+ cond: { $gte: ["$$point.time", fromDate] },
+ },
+ },
+ },
+ },
+ {
+ $addFields: {
+ dataSize: { $size: "$datapoints" },
+ interval: {
+ $max: [
+ 1,
+ { $ceil: { $divide: [{ $size: "$datapoints" }, 1000] } },
+ ],
+ },
+ },
+ },
+ {
+ $project: {
+ selectedData: {
+ $reduce: {
+ input: { $range: [0, "$dataSize", "$interval"] },
+ initialValue: [],
+ in: {
+ $concatArrays: [
+ "$$value",
+ [
+ {
+ $arrayElemAt: ["$datapoints", "$$this"],
+ },
+ ],
+ ],
+ },
+ },
+ },
+ },
+ },
+ ]);
+
+ return {
+ _id: device._id,
+ serial: device.serial,
+ name: device.name,
+ remote: device.remote,
+ data: {
+ datapoints: data.length > 0 ? data[0].selectedData : [],
+ },
+ };
+ })
+ );
+
+ return {
+ _id: apiary._id,
+ name: apiary.name,
+ location: apiary.location,
+ members: apiary.members,
+ devices: updatedDevices,
+ };
+ })
+ );
+
+ res.status(200).json(updatedApiaries);
+});
+
+// @status WORKING
+// @desc Set apiary
+// @route POST /api/apiaries
+// @access Private; all users
+const setApiary = asyncHandler(async (req, res) => {
+ // Get info from body
+ const { name, location } = req.body;
+
+ // If missing any req fields, error
+ if (!name || !location) {
+ res.status(400);
+ throw new Error("Please include all required fields");
+ }
+
+ // Check if apiary exists
+ const apiaryExists = await Apiary.findOne({ name });
+
+ // If apiaryExists, error
+ if (apiaryExists) {
+ res.status(400);
+ throw new Error("Apiary already exists");
+ }
+
+ // Create apiary
+ // .create won't work
+ // find by an empty id (won't find one), so instead of updating, it "upserts" (creates) a new one
+ const apiary = await Apiary.findOneAndUpdate(
+ { _id: mongoose.Types.ObjectId() },
+ {
+ name: name,
+ location: location,
+ members: {
+ user: req.user.id,
+ role: "CREATOR",
+ },
+ devices: [],
+ },
+ {
+ new: true,
+ upsert: true,
+ }
+ ).populate("members.user");
+
+ res.status(200).json(apiary);
+});
+
+// @status WORKING
+// @desc Update apiary
+// @route PUT /api/apiaries/:apiary_id
+// @access Private; admins of apiary only
+const updateApiary = asyncHandler(async (req, res) => {
+ const { user, role } = await checkUserToApiary(req, res);
+
+ // If not an admin or not the currently logged in user, unauthorized
+ if (
+ (role != "CREATOR" && role != "ADMIN") ||
+ user.toString() !== req.user.id
+ ) {
+ res.status(400);
+ throw new Error(
+ "User not authorized. User must be an admin of the apiary to update it"
+ );
+ }
+
+ // Update the apiary accordingly (only update name, location)
+ // Cannot update arrays directly (prevent overwriting array)
+ const updatedApiary = await Apiary.findByIdAndUpdate(
+ { _id: req.params.apiary_id },
+ {
+ $set: {
+ name: req.body.name,
+ //location: req.body.location,
+ },
+ },
+ {
+ new: true,
+ }
+ ).populate("members.user");
+
+ res.status(200).json(updatedApiary);
+});
+
+// @status WORKING
+// @desc Delete apiary
+// @route DELETE /api/apiaries/:apiary_id
+// @access Private; creator of apiary only
+const deleteApiary = asyncHandler(async (req, res) => {
+ const { user, role, apiary } = await checkUserToApiary(req, res);
+
+ // If not the creator or not the currently logged in user, unauthorized
+ if (role != "CREATOR" || user.toString() !== req.user.id) {
+ res.status(400);
+ throw new Error(
+ "User not authorized. User must be the original creator of the apiary to delete it"
+ );
+ }
+
+ // Delete all data associated with apiary
+ await Data.deleteMany({ apiary: apiary._id });
+
+ // Delete apiary
+ await apiary.remove();
+
+ res.status(200).json({ _id: req.params.apiary_id });
+});
+
+// @status WORKING
+// @desc Set device
+// @route PUT /api/apiaries/device/:apiary_id
+// @access Private; admins of apiary only
+const setDevice = asyncHandler(async (req, res) => {
+ const { user, role } = await checkUserToApiary(req, res);
+ const { serial, name, remote } = req.body;
+
+ // If not an admin or not the currently logged in user, unauthorized
+ if (
+ (role != "CREATOR" && role != "ADMIN") ||
+ user.toString() !== req.user.id
+ ) {
+ res.status(400);
+ throw new Error(
+ "User not authorized. User must be an admin of the apiary to update it"
+ );
+ }
+
+ // Check if device exists
+ const deviceExists = await Apiary.findOne({
+ devices: {
+ $elemMatch: {
+ serial: req.body.serial,
+ },
+ },
+ });
+
+ // If deviceExists, error
+ if (deviceExists) {
+ res.status(400);
+ throw new Error("Device already exists");
+ }
+
+ const newData = await Data.create({
+ serial: serial,
+ apiary: req.params.apiary_id,
+ data: {},
+ });
+
+ const updatedApiary = await Apiary.findByIdAndUpdate(
+ { _id: req.params.apiary_id },
+ {
+ $push: {
+ devices: {
+ serial: serial,
+ name: name,
+ remote: remote,
+ data: newData._id,
+ },
+ },
+ },
+ {
+ new: true,
+ }
+ ).populate("members.user");
+
+ res.status(200).json(updatedApiary);
+});
+
+// @status WORKING
+// @desc Update device
+// @route PUT /api/apiaries/device/:apiary_id&:device_id
+// @access Private; admins of apiary only
+const updateDevice = asyncHandler(async (req, res) => {
+ const { user, role } = await checkUserToApiary(req, res);
+ const { serial, name, remote } = req.body;
+
+ // If not an admin or not the currently logged in user, unauthorized
+ if (
+ (role != "CREATOR" && role != "ADMIN") ||
+ user.toString() !== req.user.id
+ ) {
+ res.status(400);
+ throw new Error(
+ "User not authorized. User must be an admin of the apiary to update it"
+ );
+ }
+
+ const updatedApiary = await Apiary.findOneAndUpdate(
+ { _id: req.params.apiary_id, "devices._id": req.params.device_id },
+ {
+ $set: {
+ "devices.$.name": name,
+ "devices.$.remote": remote,
+ },
+ },
+ {
+ new: true,
+ }
+ ).populate("members.user");
+
+ if (!updatedApiary) {
+ res.status(400);
+ throw new Error("Device was not found");
+ }
+
+ res.status(200).json(updatedApiary);
+});
+
+// @status WORKING
+// @desc Delete device
+// @route DELETE /api/apiaries/device/:apiary_id&:device_id
+// @access Private; admins of apiary only
+const deleteDevice = asyncHandler(async (req, res) => {
+ const { user, role } = await checkUserToApiary(req, res);
+
+ // If not an admin or not the currently logged in user, unauthorized
+ if (
+ (role != "CREATOR" && role != "ADMIN") ||
+ user.toString() !== req.user.id
+ ) {
+ res.status(400);
+ throw new Error(
+ "User not authorized. User must be an admin of the apiary to update it"
+ );
+ }
+
+ const updatedApiary = await Apiary.findByIdAndUpdate(
+ { _id: req.params.apiary_id },
+ {
+ $pull: {
+ devices: {
+ _id: req.params.device_id,
+ },
+ },
+ },
+ {
+ new: true,
+ }
+ ).populate("members.user");
+
+ if (!updatedApiary) {
+ res.status(400);
+ throw new Error("Device was not found");
+ }
+
+ const deletedData = await Data.findOneAndDelete({
+ serial: req.params.serial,
+ });
+
+ res.status(200).json(updatedApiary);
+});
+
+// @status WORKING
+// @desc Update members to apiary
+// @route PUT /api/apiaries/member/:apiary_id&:user_id&setEditor
+// @access Private; admins of apiary only
+const setMember = asyncHandler(async (req, res) => {
+ const { user, role, apiary } = await checkUserToApiary(req, res);
+
+ // If not an admin or not the currently logged in user, unauthorized
+ if (
+ (role != "CREATOR" && role != "ADMIN") ||
+ user.toString() !== req.user.id
+ ) {
+ res.status(400);
+ throw new Error(
+ "User not authorized. User must be an admin of the apiary to update it"
+ );
+ }
+
+ const newMember = await User.findOne({ email: req.body.email });
+ var found = false;
+ apiary.members.forEach((member) => {
+ if (member.user._id.toString() === newMember._id.toString()) {
+ found = true;
+ res.status(400);
+ throw new Error("User is already a member of this apiary");
+ }
+ });
+
+ var updatedApiary;
+
+ if (!found && req.body.role != "CREATOR") {
+ // Push the new member :user_id to the apiary :apiary_id
+ updatedApiary = await Apiary.findByIdAndUpdate(
+ { _id: req.params.apiary_id },
+ {
+ $push: {
+ members: {
+ user: newMember,
+ role: req.body.role,
+ },
+ },
+ },
+ {
+ new: true,
+ }
+ ).populate("members.user");
+
+ res.status(200).json(updatedApiary);
+ } else {
+ res.status(400);
+ throw new Error("User role cannot be set to CREATOR");
+ }
+});
+
+// @status WORKING
+// @desc Update members to apiary
+// @route PUT /api/apiaries/member/:apiary_id&:user_id&setOwner
+// @access Private; admins of apiary only
+const updateMember = asyncHandler(async (req, res) => {
+ const { user, role, apiary } = await checkUserToApiary(req, res);
+ // If not an admin or not the currently logged in user, unauthorized
+ if (
+ (role != "CREATOR" && role != "ADMIN") ||
+ user.toString() !== req.user.id
+ ) {
+ res.status(400);
+ throw new Error(
+ "User not authorized. User must be an admin of the apiary to update it"
+ );
+ }
+
+ var found = false;
+ apiary.members.forEach((member) => {
+ if (member.user.toString() === req.params.user_id) {
+ found = true;
+ return;
+ }
+ });
+
+ var updatedApiary;
+
+ if (found && req.body.role != "CREATOR") {
+ updatedApiary = await Apiary.findOneAndUpdate(
+ { _id: req.params.apiary_id, "members.user": req.params.user_id },
+ {
+ $set: {
+ "members.$.role": req.body.role,
+ },
+ },
+ {
+ new: true,
+ }
+ ).populate("members.user");
+ res.status(200).json(updatedApiary);
+ } else {
+ res.status(400);
+ throw new Error("User not found");
+ }
+});
+
+// @status WORKING
+// @desc Delete member from apiary
+// @route PUT /api/apiaries/member/:apiary_id&:user_id
+// @access Private; admins of apiary only
+const deleteMember = asyncHandler(async (req, res) => {
+ const { user, role, apiary } = await checkUserToApiary(req, res);
+
+ // If not the admin or not the currently logged in user, unauthorized
+ if (
+ (role != "CREATOR" && role != "ADMIN") ||
+ user.toString() !== req.user.id
+ ) {
+ res.status(400);
+ throw new Error(
+ "User not authorized. User must be an admin of the apiary to update it"
+ );
+ }
+
+ apiary.members.forEach((member) => {
+ if (
+ member.user.toString() === req.params.user_id &&
+ member.role == "CREATOR"
+ ) {
+ res.status(400);
+ throw new Error("The creator of the apiary cannot be deleted");
+ }
+ });
+
+ const updatedApiary = await Apiary.findByIdAndUpdate(
+ { _id: req.params.apiary_id },
+ {
+ $pull: {
+ members: {
+ user: req.params.user_id,
+ },
+ },
+ },
+ {
+ new: true,
+ }
+ ).populate("members.user");
+
+ if (!updatedApiary) {
+ res.status(400);
+ throw new Error("Member was not found");
+ }
+
+ res.status(200).json(updatedApiary);
+});
+
+module.exports = {
+ getApiaries,
+ getApiaryWithDeviceData,
+ setApiary,
+ updateApiary,
+ deleteApiary,
+ setDevice,
+ updateDevice,
+ deleteDevice,
+ setMember,
+ updateMember,
+ deleteMember,
+};
diff --git a/backend/controllers/data.controller.js b/backend/controllers/data.controller.js
new file mode 100644
index 0000000..46e0fe9
--- /dev/null
+++ b/backend/controllers/data.controller.js
@@ -0,0 +1,88 @@
+const asyncHandler = require("express-async-handler");
+const Apiary = require("../models/apiary.model.js");
+const Data = require("../models/data.model.js");
+
+// get n last data points from array
+const getData = asyncHandler(async (req, res) => {
+ const limit = req.query.limit === undefined ? 100 : req.query.limit;
+
+ // Check if device exists
+ const deviceExists = await Apiary.findOne({
+ devices: {
+ $elemMatch: {
+ serial: req.params.serial,
+ },
+ },
+ });
+
+ // If !deviceExists, error
+ if (!deviceExists) {
+ res.status(400);
+ throw new Error("Device does not exist");
+ }
+
+ const data = await Data.findOne({ serial: req.params.serial }, {datapoints: { $slice: -limit }});
+
+ if (limit > data.datapoints.length) {
+ res.status(204).send();
+ return;
+ }
+
+ res.status(200).json(data.datapoints);
+});
+
+// @status DONE
+// @desc Upload data point
+// @route POST /api/data/serial/:serial
+// @access NEEDS PROTECTION (ML TEAM AUTHORIZED ONLY)
+const putData = asyncHandler(async (req, res) => {
+ let {
+ time,
+ raw_activity,
+ weather,
+ prediction_activity,
+ last_prediction_deviation,
+ } = req.body;
+
+ last_prediction_deviation ??= 0;
+
+ // Check if device exists
+ const deviceExists = await Apiary.findOne({
+ devices: {
+ $elemMatch: {
+ serial: req.params.serial,
+ },
+ },
+ });
+
+ // If !deviceExists, error
+ if (!deviceExists) {
+ res.status(400);
+ throw new Error("Device does not exist");
+ }
+
+ const updatedReport = await Data.updateOne(
+ { serial: req.params.serial },
+ {
+ $push: {
+ datapoints: {
+ time: time,
+ raw_activity: raw_activity,
+ weather: weather,
+ prediction_activity: prediction_activity,
+ last_prediction_deviation: last_prediction_deviation,
+ },
+ },
+ },
+ {
+ new: true,
+ }
+ );
+
+ res.status(200).json(updatedReport);
+});
+
+module.exports = {
+ getData,
+ putData,
+};
diff --git a/backend/controllers/user.controller.js b/backend/controllers/user.controller.js
new file mode 100644
index 0000000..638d8fc
--- /dev/null
+++ b/backend/controllers/user.controller.js
@@ -0,0 +1,101 @@
+const jwt = require("jsonwebtoken");
+const bcrypt = require("bcryptjs");
+const asyncHandler = require("express-async-handler");
+const User = require("../models/user.model");
+
+// @desc Register new user
+// @route POST /api/users
+// @access Public
+const registerUser = asyncHandler(async (req, res) => {
+ const { name, email, password } = req.body;
+
+ if (!name || !email || !password) {
+ res.status(400);
+ throw new Error("Please include all required fields");
+ }
+
+ // Check if user exists
+ const userExists = await User.findOne({ email });
+
+ if (userExists) {
+ res.status(400);
+ throw new Error("User already exists");
+ }
+
+ // Hash password w/ bcrypt
+ const salt = await bcrypt.genSalt(10);
+ const hashedPassword = await bcrypt.hash(password, salt);
+
+ // Create user
+ const user = await User.create({
+ name,
+ email,
+ password: hashedPassword,
+ });
+
+ // Check for user registration success
+ if (user) {
+ res.status(201).json({
+ _id: user.id,
+ name: user.name,
+ email: user.email,
+ token: generateToken(user._id),
+ });
+ } else {
+ res.status(400);
+ throw new Error("Invalid user data");
+ }
+});
+
+// @desc Authenticate a user (login)
+// @route POST /api/users/login
+// @access Public
+const loginUser = asyncHandler(async (req, res) => {
+ const { email, password } = req.body;
+
+ if (!email || !password) {
+ res.status(400);
+ throw new Error("Please include all required fields");
+ }
+
+ // Check that user exists by email
+ const user = await User.findOne({ email });
+
+ if (user && (await bcrypt.compare(password, user.password))) {
+ res.status(201).json({
+ _id: user.id,
+ name: user.name,
+ email: user.email,
+ token: generateToken(user._id),
+ });
+ } else {
+ res.status(400);
+ throw new Error("Invalid email or password");
+ }
+});
+
+// @desc Get user data
+// @route GET /api/users/me
+// @access Private
+const getMe = asyncHandler(async (req, res) => {
+ const { _id, name, email } = await User.findById(req.user.id);
+
+ res.status(200).json({
+ id: _id,
+ name,
+ email,
+ });
+});
+
+// Generate JWT
+const generateToken = (id) => {
+ return jwt.sign({ id }, process.env.JWT_SECRET, {
+ expiresIn: "30d",
+ });
+};
+
+module.exports = {
+ registerUser,
+ loginUser,
+ getMe,
+};
diff --git a/backend/middleware/auth.middleware.js b/backend/middleware/auth.middleware.js
new file mode 100644
index 0000000..e144a46
--- /dev/null
+++ b/backend/middleware/auth.middleware.js
@@ -0,0 +1,36 @@
+const jwt = require("jsonwebtoken");
+const asyncHandler = require("express-async-handler");
+const User = require("../models/user.model.js");
+
+const protect = asyncHandler(async (req, res, next) => {
+ let token;
+
+ if (
+ req.headers.authorization &&
+ req.headers.authorization.startsWith("Bearer")
+ ) {
+ try {
+ // Get token from header
+ token = req.headers.authorization.split(" ")[1];
+
+ // Verify token
+ const decoded = jwt.verify(token, process.env.JWT_SECRET);
+
+ // Get user from the token
+ req.user = await User.findById(decoded.id).select("-password");
+
+ next();
+ } catch (error) {
+ console.log(error);
+ res.status(401);
+ throw new Error("Not authorized");
+ }
+ }
+
+ if (!token) {
+ res.status(401);
+ throw new Error("Not authorized, no token");
+ }
+});
+
+module.exports = { protect };
diff --git a/backend/middleware/error.middleware.js b/backend/middleware/error.middleware.js
new file mode 100644
index 0000000..c4d33de
--- /dev/null
+++ b/backend/middleware/error.middleware.js
@@ -0,0 +1,14 @@
+const errorHandler = (err, req, res, next) => {
+ const statusCode = res.statusCode ? res.statusCode : 500;
+
+ res.status(statusCode);
+
+ res.json({
+ message: err.message,
+ stack: process.env.NODE_ENV === "production" ? null : err.stack,
+ });
+};
+
+module.exports = {
+ errorHandler,
+};
diff --git a/backend/models/apiary.model.js b/backend/models/apiary.model.js
new file mode 100644
index 0000000..aa5c4b3
--- /dev/null
+++ b/backend/models/apiary.model.js
@@ -0,0 +1,110 @@
+const mongoose = require("mongoose");
+
+const geoSchema = mongoose.Schema({
+ type: {
+ type: String,
+ default: "Point",
+ },
+ coordinates: {
+ type: [Number], //the type is an array of numbers
+ index: "2dsphere",
+ },
+ formattedAddress: {
+ type: String,
+ },
+ placeID: {
+ type: String,
+ },
+});
+
+const memberSchema = mongoose.Schema({
+ user: {
+ type: mongoose.Schema.Types.ObjectId,
+ required: [true, "Please add a creator"],
+ ref: "User",
+ },
+ role: {
+ type: String,
+ default: "USER",
+ enum: ["USER", "ADMIN", "CREATOR"],
+ },
+});
+
+const dataSchema = mongoose.Schema(
+ {
+ time: Date,
+ intake: Number,
+ outtake: Number,
+ },
+ {
+ timeseries: {
+ timeField: "time",
+ granularity: "minutes",
+ },
+ }
+);
+
+const deviceSchema = new mongoose.Schema(
+ {
+ serial: {
+ type: String,
+ required: [true, "Please add a serial number"],
+ unique: true,
+ // partialFilterExpression: { serial: { $type: "string" } },
+ sparse: true,
+ immutable: true,
+ },
+ name: {
+ type: String,
+ required: [true, "Please add a name"],
+ },
+ remote: {
+ type: String,
+ required: [true, "Please add a remote.it URL"],
+ unique: true,
+ // partialFilterExpression: { remote: { $type: "string" } },
+ sparse: true,
+ },
+ data: {
+ type: mongoose.Schema.Types.ObjectId,
+ ref: "Data",
+ },
+ },
+ {
+ timestamps: true,
+ }
+);
+
+function arrayLimit(val) {
+ return val.length <= 10;
+}
+
+const apiarySchema = new mongoose.Schema(
+ {
+ name: {
+ type: String,
+ // required: [true, "Please add a name"],
+ },
+ location: {
+ type: geoSchema,
+ // required: [true, "Please add a location"],
+ },
+ members: [
+ {
+ type: memberSchema,
+ required: [true, "Please add a member"],
+ validate: [arrayLimit, "Members exceeds the limit of 10"],
+ },
+ ],
+ devices: [
+ {
+ type: deviceSchema,
+ },
+ ],
+ },
+ {
+ timestamps: true,
+ }
+);
+
+module.exports = mongoose.model("Apiary", apiarySchema);
diff --git a/backend/models/data.model.js b/backend/models/data.model.js
new file mode 100644
index 0000000..ad5b645
--- /dev/null
+++ b/backend/models/data.model.js
@@ -0,0 +1,45 @@
+const mongoose = require("mongoose");
+
+// Define the data point schema
+const dataPointSchema = new mongoose.Schema({
+ time: { type: Date, default: Date.now },
+ raw_activity: {
+ x: { type: Number, required: true },
+ y: { type: Number, required: true },
+ },
+ weather: {
+ temp: { type: Number, required: true },
+ humidity: { type: Number, required: true },
+ windspeed: { type: Number, required: true },
+ },
+ prediction_activity: {
+ x: { type: Number, required: true },
+ y: { type: Number, required: true },
+ },
+ last_prediction_deviation: {
+ type: Number,
+ required: false,
+ },
+});
+
+const dataSchema = new mongoose.Schema({
+ apiary: {
+ type: mongoose.Schema.Types.ObjectId,
+ ref: "Apiary",
+ },
+ serial: {
+ type: String,
+ required: [true, "Please add a serial number"],
+ unique: true,
+ // partialFilterExpression: { serial: { $type: "string" } },
+ sparse: true,
+ immutable: true,
+ },
+ datapoints: [
+ {
+ type: dataPointSchema,
+ },
+ ],
+});
+
+module.exports = mongoose.model("Data", dataSchema);
diff --git a/backend/models/user.model.js b/backend/models/user.model.js
new file mode 100644
index 0000000..d372a4b
--- /dev/null
+++ b/backend/models/user.model.js
@@ -0,0 +1,24 @@
+const mongoose = require("mongoose");
+
+const userSchema = new mongoose.Schema(
+ {
+ name: {
+ type: String,
+ required: [true, "Please add a username"],
+ },
+ email: {
+ type: String,
+ required: [true, "Please add an email"],
+ unique: true,
+ },
+ password: {
+ type: String,
+ required: [true, "Please add a password"],
+ },
+ },
+ {
+ timestamps: true,
+ }
+);
+
+module.exports = mongoose.model("User", userSchema);
diff --git a/backend/routes/apiary.routes.js b/backend/routes/apiary.routes.js
new file mode 100644
index 0000000..819f682
--- /dev/null
+++ b/backend/routes/apiary.routes.js
@@ -0,0 +1,51 @@
+const router = require("express").Router();
+const {
+ getApiaries,
+ getApiaryWithDeviceData,
+ setApiary,
+ updateApiary,
+ deleteApiary,
+ setDevice,
+ updateDevice,
+ deleteDevice,
+ setMember,
+ updateMember,
+ deleteMember,
+} = require("../controllers/apiary.controller.js");
+
+const { protect } = require("../middleware/auth.middleware");
+
+// apiary requests
+router.get("/", protect, getApiaries);
+router.get("/filter/:filter", protect, getApiaryWithDeviceData);
+router.post("/", protect, setApiary);
+router.put("/apiary/:apiary_id", protect, updateApiary);
+router.delete("/apiary/:apiary_id", protect, deleteApiary);
+
+// device requests
+router.put("/apiary/:apiary_id/setdevice", protect, setDevice);
+router.put(
+ "/apiary/:apiary_id/device/:device_id/updatedevice",
+ protect,
+ updateDevice
+);
+router.put(
+ "/apiary/:apiary_id/device/:device_id/serial/:serial/deletedevice",
+ protect,
+ deleteDevice
+);
+
+// member requests
+router.put("/apiary/:apiary_id/setmember", protect, setMember);
+router.put(
+ "/apiary/:apiary_id/user/:user_id/updatemember",
+ protect,
+ updateMember
+);
+router.put(
+ "/apiary/:apiary_id/user/:user_id/deletemember",
+ protect,
+ deleteMember
+);
+
+module.exports = router;
diff --git a/backend/routes/data.routes.js b/backend/routes/data.routes.js
new file mode 100644
index 0000000..f3ff0c3
--- /dev/null
+++ b/backend/routes/data.routes.js
@@ -0,0 +1,7 @@
+const router = require("express").Router();
+const { getData, putData } = require("../controllers/data.controller.js");
+
+router.get("/serial/:serial", getData);
+router.put("/serial/:serial", putData);
+
+module.exports = router;
diff --git a/backend/routes/user.routes.js b/backend/routes/user.routes.js
new file mode 100644
index 0000000..466ee09
--- /dev/null
+++ b/backend/routes/user.routes.js
@@ -0,0 +1,13 @@
+const router = require("express").Router();
+const {
+ registerUser,
+ loginUser,
+ getMe,
+} = require("../controllers/user.controller.js");
+const { protect } = require("../middleware/auth.middleware.js");
+
+router.post("/register", registerUser);
+router.post("/login", loginUser);
+router.get("/me", protect, getMe);
+
+module.exports = router;
diff --git a/backend/server.js b/backend/server.js
new file mode 100644
index 0000000..0a9efa2
--- /dev/null
+++ b/backend/server.js
@@ -0,0 +1,45 @@
+const path = require("path");
+const express = require("express");
+const mongoose = require("mongoose");
+const colors = require("colors");
+const dotenv = require("dotenv");
+dotenv.config({ path: __dirname + "/.env" });
+const connectDB = require("./config/db");
+const { errorHandler } = require("./middleware/error.middleware");
+const port = process.env.PORT || 5000;
+
+connectDB();
+
+const app = express();
+
+app.use(express.json());
+app.use(express.urlencoded({ extended: false }));
+
+const userRouter = require("./routes/user.routes.js");
+const apiaryRouter = require("./routes/apiary.routes.js");
+const dataRouter = require("./routes/data.routes.js");
+
+
+app.use("/api/users", userRouter);
+app.use("/api/apiaries", apiaryRouter);
+app.use("/api/data", dataRouter);
+
+
+// Serve frontend
+if (process.env.NODE_ENV === "production") {
+ app.use(express.static(path.join(__dirname, "../frontend/build")));
+
+ app.get("*", (req, res) =>
+ res.sendFile(
+ path.resolve(__dirname, "../", "frontend", "build", "index.html")
+ )
+ );
+} else {
+ app.get("/", (req, res) => res.send("Please set to production"));
+}
+
+app.use(errorHandler);
+
+app.listen(port, () => {
+ console.log(`Server is running on port: ${port}`);
+});
diff --git a/frontend/.gitignore b/frontend/.gitignore
new file mode 100644
index 0000000..4d29575
--- /dev/null
+++ b/frontend/.gitignore
@@ -0,0 +1,23 @@
+# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
+
+# dependencies
+/node_modules
+/.pnp
+.pnp.js
+
+# testing
+/coverage
+
+# production
+/build
+
+# misc
+.DS_Store
+.env.local
+.env.development.local
+.env.test.local
+.env.production.local
+
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
diff --git a/frontend/README.md b/frontend/README.md
new file mode 100644
index 0000000..de6c0e1
--- /dev/null
+++ b/frontend/README.md
@@ -0,0 +1,46 @@
+# Getting Started with Create React App and Redux
+
+This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app), using the [Redux](https://redux.js.org/) and [Redux Toolkit](https://redux-toolkit.js.org/) template.
+
+## Available Scripts
+
+In the project directory, you can run:
+
+### `npm start`
+
+Runs the app in the development mode.\
+Open [http://localhost:3000](http://localhost:3000) to view it in your browser.
+
+The page will reload when you make changes.\
+You may also see any lint errors in the console.
+
+### `npm test`
+
+Launches the test runner in the interactive watch mode.\
+See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
+
+### `npm run build`
+
+Builds the app for production to the `build` folder.\
+It correctly bundles React in production mode and optimizes the build for the best performance.
+
+The build is minified and the filenames include the hashes.\
+Your app is ready to be deployed!
+
+See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
+
+### `npm run eject`
+
+**Note: this is a one-way operation. Once you `eject`, you can't go back!**
+
+If you aren't satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
+
+Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you're on your own.
+
+You don't have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn't feel obligated to use this feature. However we understand that this tool wouldn't be useful if you couldn't customize it when you are ready for it.
+
+## Learn More
+
+You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
+
+To learn React, check out the [React documentation](https://reactjs.org/).
diff --git a/frontend/package.json b/frontend/package.json
new file mode 100644
index 0000000..c50aeb1
--- /dev/null
+++ b/frontend/package.json
@@ -0,0 +1,68 @@
+{
+ "name": "frontend",
+ "version": "0.1.0",
+ "proxy": "http://localhost:5000",
+ "private": true,
+ "dependencies": {
+ "@emotion/react": "^11.10.6",
+ "@emotion/styled": "^11.10.6",
+ "@mui/icons-material": "^5.11.9",
+ "@mui/joy": "^5.0.0-alpha.67",
+ "@mui/material": "^5.11.9",
+ "@reduxjs/toolkit": "^1.9.2",
+ "@testing-library/jest-dom": "^5.16.5",
+ "@testing-library/react": "^13.4.0",
+ "@testing-library/user-event": "^14.4.3",
+ "@types/jest": "^29.5.1",
+ "@types/node": "^18.16.3",
+ "@types/react": "^18.2.3",
+ "@types/react-dom": "^18.2.3",
+ "autosuggest-highlight": "^3.3.4",
+ "axios": "^1.3.3",
+ "date-fns": "^2.29.3",
+ "lodash": "^4.17.21",
+ "material-ui-autocomplete-google-places": "^2.3.0",
+ "mui-address-autocomplete": "^3.0.4",
+ "mui-places-autocomplete": "^2.0.0",
+ "react": "^18.2.0",
+ "react-dom": "^18.2.0",
+ "react-icons": "^4.7.1",
+ "react-open-weather": "^1.3.0",
+ "react-places-autocomplete": "^7.3.0",
+ "react-pro-sidebar": "^0.7.1",
+ "react-redux": "^8.0.5",
+ "react-router-dom": "^6.8.1",
+ "react-scripts": "5.0.1",
+ "react-toastify": "^9.1.1",
+ "recharts": "^2.5.0",
+ "typescript": "^5.0.4",
+ "web-vitals": "^2.1.4"
+ },
+ "scripts": {
+ "start": "react-scripts start",
+ "build": "react-scripts build",
+ "test": "react-scripts test",
+ "eject": "react-scripts eject"
+ },
+ "eslintConfig": {
+ "extends": [
+ "react-app",
+ "react-app/jest"
+ ]
+ },
+ "browserslist": {
+ "production": [
+ ">0.2%",
+ "not dead",
+ "not op_mini all"
+ ],
+ "development": [
+ "last 1 chrome version",
+ "last 1 firefox version",
+ "last 1 safari version"
+ ]
+ },
+ "devDependencies": {
+ "sass": "^1.60.0"
+ }
+}
diff --git a/frontend/public/about-ss.PNG b/frontend/public/about-ss.PNG
new file mode 100644
index 0000000000000000000000000000000000000000..9fdcd76c15e3178599d8827296f1e9be6295bff6
GIT binary patch
literal 64242
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
FKL7%Z%+3G+
literal 0
HcmV?d00001
diff --git a/frontend/public/db-ss.PNG b/frontend/public/db-ss.PNG
new file mode 100644
index 0000000000000000000000000000000000000000..4bcaf4bea2c2a9914efeecce164be903bb1ff9d9
GIT binary patch
literal 249602
zcmeEucUV(d_wE5v5Cud>P-!}-DBVUCVuf)~utDenB%<_U=p?A4s5Bj#ihzm=N(n^>
zH9={bAc#l_0fJ(P5+IO3LP)zOfHUKK^L_Vs|GLj}?{}WZlD5y@Yp=c5yWX|dxp~ss
zQbu}{GyniH$9_3-8URH9UH(vN1^CbJz$P;Ik66fQ%R@kMyYe^ig@o6^69)mHGrjX!$mki{YK@?$H+CRY%DsqE8UxpfUe%CP|M{!m9^
zwUp1MAQHy^n{p@!
z8F+RWFVm}f%A8-COE6R=o^<79o}lD)sP(QDCR06Jwbv2O!;i0}Yv_5MKF;OJ1Inj+
z^1o|GgIh6{F~#V+N+Wa#SXGysd_Gq=+#C7Ae;EOpUHijV51?F8(^^isg>d(RgGV-=
zva3wm9sx0?=c$Uv9>#B4|9wb%Vh(aDf3NetgdKaFpzG>#(jp+a+W?~{g{Q=pz7spt
zqcUA)JBX}+oeC{fcM4Of%Xt+J$yNXHTh08s}^%_ikF30K&A_S3zL{x;q5Sw-A#
zndAGm^kj8k+%VMfH5^i~IE=>Y_Bgy8l9$35(M=w_qg|W8hG1VNs0rtuGX$Ov-zV{v
zqLl3WOXF#NRU|b%-PaZ3G3bzWeODWIW6#$595p1rit({g5>6;=VmX+JHEe+=n#fB4
z-(_)k4HOm;zA1IAmqYcv_;z$wYwUKn-@fZH5KO(F6*0=v91TlhAS-=|Wxq&>ms9L^
z7qgi}2mwxz^+=_A8855`@v{{0-2&)uQH=_W+4MW!Q><(QU++-JV29jVgLo59gj3?d
zAsb9Sv&6kxeJ6#=8FdF;4J7~?-x*ThH&3KbI
zDZ#aT)cQjN*Ye-Nwb+uqm28D*@%vdF%`fLsc6QI+vd=6Bpz{x8&izF9+IDUwfc^Fk
zb^AB`ydt^-uM$^yO5^o!%KowrzcapT!FEC(B=pjzq_L;cDYnV#`L@ePO|AwPY<#M-
zxJYcy8hV~|BO>WW@5cL1C)N~+R#}3?c8*M&Ix*#pxpIUU
z)a>gIoV0304AUx#iFnt|9>vYk7*~bab;XPUScVOj|5)sBd`MDF%>d`ASB9Rc@*FZ^
z{G4HWIagZqy9OS|2JA1}B4at5D}5hvi)yFTe``TQyoYSo8xZ{CNHMg5gL^s17p&w;
zbQ3e{GEQ@i{kwu8r?vvTgLMks>o(w4mpoQ4<00CqB?V6~2C8OT;_!w-gQ$t!0`4kG
zshn%|p{imi5Ex+0y=s%TY_NmPAmd<@Q6sP9*|J2zG-C#s<-by=>p3ZQ7i1vdT<3bb
z)dB>UBi6delbHn>Ig=&$yBF$JTBXl-oee$xQ#>v0DU$L5AZv^H7+Q%x!D
z-h(Y+6`yAvsz+|`8#}VW2?%tFXf*3!sWn+?s}LWeXfrIQhQ(7`K}
z%<9Qr*4l+>Bf*m%u0~kSd*MVm&rI){;XXeUV!Ni88DmS@o?T$kMsu!IJAuu4fy@$X
z$Pb%!7*il5jVzu&&Qkk&w$|`(-j-81#Il#H)lpANc5=#RgUDS
z7yDVxjA+=;57}c+lKgTn#&d3aoHk#YWy9XD8nde&ubK6O7-%v1Bk{Uh!h|y;V`z6CC>QS_y$YH7|85ow!)=B0!
zw<`89({PUdW>#iwsOaSRPzU(KuncN?Z46xw>pb-GNm59RU>&bM4o@MPe3$>T0}za7;sM*QtK*Npe-ZKo_bbA*XDo=-?|Q$k
z**7mgKt)VsPS@X#zqtOxxH7S1lbHS}LYK6-G+VyOve;?i81th}x(vau)qhD{0@FP~
z#}Fw2
zKEiSUrfoy7-mPLhb$fzPn8z5%AjstD#!R5*D?$h|u&)R$vLb;>?yrxgSK6~!EqOzy
z&B^EXzRrUTL_GoV@4^*(jFo6<@Z?i|+Sx}{n!XAzs4G^wBfg*Aj$aYQLr{YNV^C`}
z^%ce1G<_YqYo+1W+nvOr9c(3WW$*~B23TeN9fLcDI(}VpAzwkjk+>BI=M6C~)%dDm
zO3V9PYSJgx13X$VbvlaxcXeSQ&7%A&
zvzwf7&azw4{OR6F=ehwM?gz@eQgeVy1k@yQxExNuMVDKeyc=U6a6FeQYLOIm
zGHQ8xuJ-f>w@1Fl4E!I@b(q1l&cBa4A@f^VxRu0JtIKZoPSJxBk)!8orqNcWn`^
zKyz>9Y5fA}dopW>7qte&1k&$hJZ`UXOQGlO$+uq)6bPXhiQ=Kd`_S{KFo$~33*46i
zlEA4p>BNFz%h-_%dx1V5&-yQRugvO=kG*=!Y^<+h
zl8w@FI4_}En_K=zG}JXxD%qPS_S|27Yy3$&_KhI<4`Z_4wqnId%Zxc2*bmav(TzupiTb*tZrK4;ZAQ_KPOA?ho+v>|-}n
zHT(LL-Gc{lClif&`O%fWdZ*$F{R?)z;eJ!NTqV_c4cQoU?^rm*)@_f)it*LU&J#q_
zST{Sa{JcbjloA+xZOer~&iXfSuMQsejJR7Q1csFqbTG0{&*TBa?~?1PF5Rm}L^FMC(pv2%2o%0zSkHGf?p9ML_zZpK
z{v3H$y$#Nsm#4Ll?M-kW>L}U%jeq1uY1#cLSV@eX&C)6?mc`A$TKYPz=dLF8rh1~spDaye9tuj6QC(7Yp~r-w&lsON$F-bHDHlv%%P53A=BS1q6AA>DtLU
z4!U*V8LIc6E_0DZjbwDyR9d`q3MP?uN&&2ztm(_up{Cf7YJVDDN%hUu+mfmgD7AyX
zem7cgF_Skk(iV`?$;8uz>ffW7X{5#BB@sPa7E4I@tSDBCuH!!5+E9sg7JSN!{7r~F
zExRA-)jNS|+$#Z~FwWrtp-RKX9Wm7dfn%THq@J{L$lO_zV7HHSW`K+Nk(Eog)Iag_
z4qNIQ-&3I}?%H%OeK6LM0%UiBF8+Nu5ZFE)mfkAsN$4=0;xlG?vo({iY2@fY=IC#C
zugcbm>UubENdgddJwn-IKNRSQ09Yn${zx@529r8r>c5jN9M-@?1*6T;L}SV90jJ8)
zjPE8+f7wD9cKL<-k$c3N`@{S~FgDUzl4grM@SvO%Q*TkK)D#~wyWda;J>cT?jV&TmF5ztwTfP1vF3WSLTz^MHLu?)y+b0jr(p8@5Z#c|?p~
zB~Dz?>RJcBL#{wmb&{Z$hS7wf34i{orLhWSz`$$ng@4y3^v&M$@T3%W*C_v^)spCU
ztpFN7Wzcn!;qKLxX!`SDDuvd?kiB0FxEXnJhy8+vX>w^w3B2zhvhZN5pUD%EDW`O)
znbz#X_s65kpYvuoBaWYi)6Q-2a1UZ(!m-c%FO=iAE}|fRqS`*uwqoP(T^;pzQ-j{i
zVL4|xUvx6k8@qPboPYr(MdZppC<_a(mS6K-|AGERj_2lKm`i1L7`*yZOcWtY?Ew?(
zgl93^bu#5nq-P=ezvFRWgV=(EL{!+~g;@7PXb+q;{O=b;lKHP37WL!5a`=Br4wzC&
zz{V7DxO%qb1m$v+K^mWHkdcvXRt(yw$~n}2MW)9+djqX=5B@@}#nvS10O9vX!SbClW8FJ@?sY%ab;wO3q51Rlrya{?a^J?>
z;IG%WW-Y}%4KEA8mCmLqb;Y`_6LEKAV61M$YY9+2jikflI=a7o4W5&y5$@0o#0q~M
zKkNZp%T4XWcyb@O_rb(c9#8UB7Uy(e(0dgyNyf}O3C(8%1<|;r!MwW!%z*mnVqMET
z6%x>RBgSg{Tv<$qx9Hd2AGxNBzmDDhaV=0M=YZ#+9fz0?yVb283-S**i7AmtZe
z&Eib%DyYi==7Basv4-~BFt0gu2y$RWRfuQ9Bs_hcD4_e*A=h9h?d6L6!`S@FCLp#H5&k8
zc4Z3s47HBpzy5(dFh0?gVy%?{o4($&HHvn6=d@UIF$!YRRL
z1>7zb1K_JdL;YY-YaAYC#_XlYkq2QLMsI1MbumM%>__584VMU@#=OBhx0?=4$HB?D
zHZQM4$;AlbDK|9)WxQ`W0?rH8;pzK*am`rmOu6?vf0QnpPI_>CdN)YuNON=y(Xdk)
zg-N-o`@B=itZ8XS){F|cYT9xT9;N4Hq~JtA&7*Pt*PL`y$V<#Pi5>1Ea3asMu0=$#
z2D;~k5mcAc=7MQRfDZFp$NrQ4IDUI@uS*!lqVLu>N%OfoJxmlM4`nBqOD3UL?z+`G
z$d2~-v`gQmEav^@vfUZ$#2O^cx$j?(^s$d@&}W^Ro~t0CcjO3W_>|7p>*Rb&lw+<8OS4>4(%!TzX2YUP8@6r&
z&ktxnI{8Z35+5@!?Bc0TH_R~n8
zu!&hpj;H3fVFqWC7~Rz|w&OSt-0b_qav4!{{v|E9yJ->l=U#d*mg#fPYf=D;`U%LmqW0Gf4uC{0Y2C5p?aVyBi+qy^fZ@!5NkmR?S9BoWssgEFCZ3^(K06
z#cAGH=YC(vc!RZTKs%oA7=k!R0T$ugEqZQIZ3ZK)X`HITKnWl0)HS_~O#d3hCq%;~
zk%a>F%~}B{WSuD`b3SJXthD+Rb7z{(cd~ZLuZgCbl1csSCi5LEh`fC=eF$e@@5Q0{YLgFl`A_
zC-$=^*#kp02(QAt`;x~q7K}w3#Z#=IFRuyv#RWAd=#hFlEyR04*c>~cT3T87iWJ~u
zrg*s_+;2UQ5aTEzWlE6QrKiQ@`H?cTSG86C<x^Z-Afm=Tjo^n&72-Pu
zcK3Ue^AFo$P7xNBE!aZ>TXlk-Z5CD`9;?RfYjwiBPLN0c%mdQ_*x5?<%k)7(jh16B
zzn)!>dby_QrrU`G?Cc{sx}VNEb+4lhXegkMpJQa;`lV$o0`nqO@0u^#Ou{!=par{)
zb&QVVUoGnVus^8a{<*NAxT&YHjGh^XGP)0>eNWdO~Q>fVc@}sFW%SPi3Ye9QCLpYC`5w
z40d%;CRs|{LVr6b^UqO?=Xr!PHhfzjP8H(3*BT9rzJB)Cbw!+Hh~R9**GF+tD}ckP
zr2*uTRa=zZOyJyy2R-4s8Z6V&oSXd46dUuw{^UBUS(tVkiq18EFRcS*(&uvJ^xERr
zz$)YlvJ1px<$2sD4dLT`kRwv__kD)SJnsF=bW2b$R4&jXN*6wtU35pTMeVoAJDMl1
zn;r(uC4NDDCGH*}ny+3o5N>O`6@a>VeBwnT^vcr7LTkjE#SQcBmInq(&p~M{T({1?
z%iq!_uCBO~wo?D*Uk>049bm)cU>C0__FR^O%h>OWrnCFj?ICcVpEB)eMTP?dHT8i5
z7DkzJ$CLlTNu4SZYnb}d_-;_z2qYOES^p;=n$H4;S2`J_^0{MjVl5^fE)FVVi`LpF
z*2Hr`qTJ!?SuC`V}tUt)*izogD3s#gU&<@XP+oK#dnQlRWZ7#
zq`!prL5l`|-+`yn&8wBJuUrBfxx0`30q~wBm;pu;OK{@-w=8-jY@YXUNl+FSPydO>
z7L)WM*@%tZ{s)~{%*_Ay^Z(D~!>ND?>tBkXZE43_)=|C)*1y&NL*KTqTwwP=v^}Hc
z8gZ#VXaWCkGypE3mLI?B&i(`bFR9afeTGVQsOz$X)8jrOtsF*8?bJhEJ5>=o-(wNIq#e-B0$_^pbILJr2&9IE(eliQ33=riv6lFK)VzPf4l
zkGN&!IXNIZbnJRCEin_njP0w*W&7#snMFBszA^o#h2grR&AUr|?OKe$x(V4`jwc4n
zbWPB55RaJtQknP3eQ{0Lt*o7hgx}!cqHfjvu_%O+6EHRJW0_JB%l3(Dh6gz%$o$#8#K6^mn?PvKGg+F4Ic8cK=S6ti!|d8aq+%QKwVhJ
zv%PJG3l%PBe*ADPu-~0<-4-lVSgLIq{NeL%RUpA_Xoo+i_ILYMqeHPjRBnN`w8xkM
z!`MUZ;!NO)f8py2_5tDa>=PGHqOC=AH{$J|^)|qm9k|jlEyHJG^%UL|tu-xA)qIDl
z7olkDl8gF-2V4qJ4d;qb=%MPTzQo{@``go1hX$Y~?-%ex?luGpT`cO_Rl>n#Nte<_0V17kzUNe5%VemTb^
zfUeXf#X7pCM4D@h;T4TSbH!WgUKCP}EaBR}EDnqh7b6&xqa|o*Mze6F+WTz77-vk_
zeF2pPrn0VF)w=nQ)5OmBsP)N(rvw@5rTiwRxG>4UMabtZZ4-C=wHzekHe7fi}i*s{hil!L$S
zqEOKG5Vqiu0J|93DmcMh1-H%-9_c}z)s5dq6d~it^Apf?nDE`(6&_;T2^YBLhgEk$^p_Z#xf&_G1Uor
z94x>_eH{8O{vm9=fcBuJ&ii9S+V?F^`6@C2csj5B(-qXhyc_x|uEg%bw*=H_PxNlc
z%Rlq+#1uI3o@~1{fs@gj%j}4p*MAB0tZi@W9YWd;>pinwQ?fSMblK%@VZnjnU%zu4
z$C#Ny4ut}bE3~44!+@8*u3i+ehLIy=61oJ5K{slSWR=x`XVb^hv*_oLv@FBp_ZNv1
z3gDLiI@-1hecvuuJalM#)qS9&RUHv;#U1*pbpIzArlL6xgcx>BMn0UaMMwuSN{t6GaxD2YB9e`ex|^JPe`X(
zE)h;9EBim;>e?4(2c%q3R`Hn!iM!ae