Revert "Adds the ability for users to create, update, and delete their own workouts"

This commit is contained in:
Rory Healy 2022-11-09 04:16:49 +00:00 committed by GitHub
parent dd0a143bef
commit 0de82a5858
12 changed files with 87 additions and 1148 deletions

View File

@ -13,88 +13,13 @@ import DropdownButton from 'react-bootstrap/DropdownButton';
import Modal from 'react-bootstrap/Modal';
// Firebase
import { deleteDoc, doc, updateDoc } from 'firebase/firestore';
import { deleteDoc, doc } from 'firebase/firestore';
import { db } from '../../firebase-config';
// Custom components
import CustomToast from './CustomToast';
// Authentication
import { useAuth } from '../../context/authUserContext';
// Makes either a button, or a dropdown button
function AButton({ type, name, id, handleModalOpen }) {
if (type === 'user') {
return (
<DropdownButton title="...">
<Link href={`/userworkouts/edit/${id}`} passHref>
<Dropdown.Item>Edit {name}</Dropdown.Item>
</Link>
<Dropdown.Item onClick={handleModalOpen}>Delete {name}</Dropdown.Item>
</DropdownButton>
);
}
if (type === 'exercise') {
return (
<DropdownButton title="...">
<Link href={`/exercises/edit/${id}`} passHref>
<Dropdown.Item>Edit {name}</Dropdown.Item>
</Link>
<Dropdown.Item onClick={handleModalOpen}>Delete {name}</Dropdown.Item>
</DropdownButton>
);
}
if (type === 'workout') {
return (
<DropdownButton title="...">
<Link href={`/workouts/edit/${id}`} passHref>
<Dropdown.Item>Edit {name}</Dropdown.Item>
</Link>
<Dropdown.Item onClick={handleModalOpen}>Delete {name}</Dropdown.Item>
</DropdownButton>
);
}
}
function DeleteModal({
name,
type,
isModalOpen,
handleModalClose,
handleDelete,
}) {
return (
<Modal show={isModalOpen} onHide={handleModalClose} centered size="lg">
<Modal.Header>
<Modal.Title>
{type === 'user' ? (
<p>Delete your workout &#39;{name}&#39;?</p>
) : (
<p>
Delete {type} &#39;{name}&#39;?
</p>
)}
</Modal.Title>
</Modal.Header>
<Modal.Footer>
<Button variant="secondary" onClick={handleModalClose}>
Close
</Button>
<Button variant="danger" onClick={handleDelete}>
Delete
</Button>
</Modal.Footer>
</Modal>
);
}
export default function EditButton({ type, id, name, onDelete }) {
const { authUser } = useAuth();
/* Handles state for the delete toast */
const [isToastActive, setToastActive] = useState({});
const handleToastOpen = ({ title, body, error }) => {
@ -112,16 +37,6 @@ export default function EditButton({ type, id, name, onDelete }) {
};
const handleDelete = () => {
if (type === 'user') {
const filtered = authUser.createdWorkouts.filter(
(workout) => workout.id !== id
);
authUser.createdWorkouts = filtered;
updateDoc(doc(db, 'users', authUser.uid), {
createdWorkouts: filtered,
});
}
if (type === 'exercise') {
deleteDoc(doc(db, 'exercises', id))
.then(() => {
@ -136,9 +51,7 @@ export default function EditButton({ type, id, name, onDelete }) {
error,
});
});
}
if (type === 'workout') {
} else {
deleteDoc(doc(db, 'workouts', id))
.then(() => {
handleToastOpen({
@ -157,6 +70,51 @@ export default function EditButton({ type, id, name, onDelete }) {
handleModalClose();
};
// Makes either a button, or a dropdown button
const makeButton = (title) => (
<DropdownButton title="...">
{title === 'exercise' ? (
<Link href={`/exercises/edit/${id}`} passHref>
<Dropdown.Item>Edit {title}</Dropdown.Item>
</Link>
) : (
<Link href={`/workouts/edit/${id}`} passHref>
<Dropdown.Item>Edit {title}</Dropdown.Item>
</Link>
)}
<Dropdown.Item onClick={handleModalOpen}>Delete {title}</Dropdown.Item>
</DropdownButton>
);
// Creates the modal only if there is an id.
const makeModal = (toShow) => {
if (toShow !== undefined) {
return (
<Modal show={isModalOpen} onHide={handleModalClose} centered size="lg">
<Modal.Header>
<Modal.Title>
<p>
Delete {type} &#39;{name}&#39;?
</p>
</Modal.Title>
</Modal.Header>
<Modal.Footer>
<Button variant="secondary" onClick={handleModalClose}>
Close
</Button>
<Button variant="danger" onClick={handleDelete}>
Delete
</Button>
</Modal.Footer>
</Modal>
);
}
return null;
};
// Creates the toast for deleting
function makeToast({ title, body, error }) {
if (title !== undefined) {
@ -171,23 +129,11 @@ export default function EditButton({ type, id, name, onDelete }) {
}
return null;
}
return (
<>
<AButton
type={type}
name={name}
id={id}
handleModalOpen={handleModalOpen}
/>
{id !== undefined && (
<DeleteModal
name={name}
type={type}
isModalOpen={isModalOpen}
handleModalClose={handleModalClose}
handleDelete={handleDelete}
/>
)}
{makeButton(type)}
{makeModal(id)}
{makeToast(isToastActive)}
</>
);

View File

@ -153,30 +153,14 @@ export default function Element({ element, type, onDelete, testAuth }) {
/>
);
}
if (type === 'workouts') {
return (
<EditButton
type="workout"
id={element.id}
name={element.name}
onDelete={onDelete}
/>
);
}
if (type.includes('user')) {
return (
<EditButton
type="user"
id={element.id}
name={element.name}
onDelete={onDelete}
/>
);
}
return null;
return (
<EditButton
type="workout"
id={element.id}
name={element.name}
onDelete={onDelete}
/>
);
};
const makeMuscles = () => {

View File

@ -1,3 +1,4 @@
/* eslint-disable no-nested-ternary */
// React
import React, { useState } from 'react';
@ -17,7 +18,7 @@ import SelectedElement from './SelectedElement';
* @param {*} listType Either "radio" or "checkbox".
* @param {*} selected State of which elements are selected. if checkbox, must be an array.
* @param {*} setSelected The function that sets the state of selected
* @param {*} type Either "exercises", "workouts", or "user" (for user workouts).
* @param {*} type Either "exercises" or "workouts"
* @param {*} onDelete The callback function to handle an element being deleted from the list.
* @returns
*/
@ -64,24 +65,6 @@ export default function List({
});
});
const placeholderText = () => {
if (filteredList.length === 0) {
if (type === 'user') {
return <h3>No user workouts available.</h3>;
}
if (type === 'workouts') {
return <h3>No workouts available.</h3>;
}
if (type === 'exercises') {
return <h3>No exercises available.</h3>;
}
}
return null;
};
return (
<div>
<SearchFilterBar
@ -97,8 +80,11 @@ export default function List({
vertical
name="button-list"
>
{placeholderText}
{filteredList.length !== 0 &&
{filteredList.length === 0 && type !== 'edit' ? (
<h3>No {type} available</h3>
) : filteredList.length === 0 && type !== 'edit' ? (
<h3>No exercise available</h3>
) : (
filteredList.map((element) => (
<ToggleButton
className={styles.list}
@ -107,7 +93,7 @@ export default function List({
name={listType}
value={element}
>
{selected && selected.name === element.name ? (
{selected.name === element.name ? (
<SelectedElement
element={element}
type={type}
@ -117,18 +103,10 @@ export default function List({
<Element element={element} type={type} onDelete={onDelete} />
)}
</ToggleButton>
))}
))
)}
</ToggleButtonGroup>
</div>
</div>
);
}
// {selected && selected.name === element.name} ? (
// <SelectedElement
// element={element}
// type={type}
// onDelete={onDelete}
// />
// ) : (
// <Element element={element} type={type} onDelete={onDelete} />)

View File

@ -147,30 +147,14 @@ export default function Element({ element, type, onDelete }) {
/>
);
}
if (type === 'workouts') {
return (
<EditButton
type="workout"
id={element.id}
name={element.name}
onDelete={onDelete}
/>
);
}
if (type.includes('user')) {
return (
<EditButton
type="user"
id={element.id}
name={element.name}
onDelete={onDelete}
/>
);
}
return null;
return (
<EditButton
type="workout"
id={element.id}
name={element.name}
onDelete={onDelete}
/>
);
};
const makeMuscles = () => {

View File

@ -20,41 +20,9 @@ import styles from '../../styles/Navbar.module.css';
// User Authentication
import { useAuth } from '../../context/authUserContext';
function NewLink() {
export default function TopNavbar() {
const router = useRouter();
const { authUser } = useAuth();
if (authUser) {
if (authUser.role === 0 && router.pathname.includes('exercises')) {
return (
<Link href="/exercises/create" passHref>
<Nav.Link className={styles.item}>New exercise</Nav.Link>
</Link>
);
}
if (router.pathname.includes('userworkouts')) {
return (
<Link href="/userworkouts/create" passHref>
<Nav.Link className={styles.item}>New user workout</Nav.Link>
</Link>
);
}
if (authUser.role === 0 && router.pathname.includes('workouts')) {
return (
<Link href="/workouts/create" passHref>
<Nav.Link className={styles.item}>New workout</Nav.Link>
</Link>
);
}
}
return null;
}
export default function TopNavbar() {
// State to keep track of whether to show sign in view
const [show, setShow] = useState(false);
const handleShow = () => setShow(true);
@ -87,7 +55,16 @@ export default function TopNavbar() {
</Nav>
<Nav>
<NewLink />
{authUser &&
(router.pathname.includes('exercises') ? (
<Link href="/exercises/create" passHref>
<Nav.Link className={styles.item}>New exercise</Nav.Link>
</Link>
) : (
<Link href="/workouts/create" passHref>
<Nav.Link className={styles.item}>New workout</Nav.Link>
</Link>
))}
<Nav.Link className={styles.item}>
{authUser === null ? (
<Nav onClick={handleShow}>Sign In</Nav>

View File

@ -21,7 +21,6 @@ const formatAuthUser = (user, id) => ({
role: user.role,
favouriteWorkouts: user.favouriteWorkouts,
favouriteExercises: user.favouriteExercises,
createdWorkouts: user.createdWorkouts,
});
const createNewUser = async (user) => {
@ -32,7 +31,6 @@ const createNewUser = async (user) => {
role: 1,
favouriteWorkouts: [],
favouriteExercises: [],
createdWorkouts: [],
});
};

View File

@ -1,18 +0,0 @@
export default function handler(req, res) {
const { body } = req;
if (!body.muscleGroups) {
body.muscleGroups = [];
}
/* Cannot have workouts without a name or exercises */
if (!body.name) {
return res.status(400).json({ error: 'Workout name required.' });
}
if (body.exercises.length < 1) {
return res.status(400).json({ error: 'Workout contains no exercises.' });
}
return res.status(200).json({ data: req.body });
}

View File

@ -1,364 +0,0 @@
// React
import React, { useEffect, useRef, useState } from 'react';
// Next components
import Link from 'next/link';
import { useRouter } from 'next/router';
// Bootstrap components
import Button from 'react-bootstrap/Button';
import Form from 'react-bootstrap/Form';
// Firebase
import {
collection,
query,
orderBy,
getDocs,
doc,
updateDoc,
} from 'firebase/firestore';
import { db } from '../../../firebase-config';
// Custom components
import AddExerciseModal from '../../../components/WorkoutForms/AddExerciseModal';
import CustomAlert from '../../../components/EditButton/CustomAlert';
import EditExerciseModal from '../../../components/WorkoutForms/EditExerciseModal';
import ExerciseElement from '../../../components/WorkoutForms/ExerciseElement';
import TopNavbar from '../../../components/Navbar/Navbar';
// Styles
import styles from '../../../styles/WorkoutForms/WorkoutForm.module.css';
// Authentication
import { useAuth } from '../../../context/authUserContext';
// Get reference to exercises and workouts collections
const exercisesCollectionRef = collection(db, 'exercises');
function WorkoutForm() {
const router = useRouter();
const { authUser } = useAuth();
/* Ensures that the database is only queried once for data */
const isFirstLoad = useRef(false);
/* Handles state for validation of form */
// TODO: Form validation
// const [validated, setValidated] = useState(false);
/* Handles state for the alert */
const [isAlertActive, setAlertActive] = useState({});
const handleAlertOpen = ({ heading, body, variant }) => {
setAlertActive({ heading, body, variant });
};
const handleAlertClose = () => {
setAlertActive({});
};
/* Used to store the ids of exercises that have been added to the workout. */
const [exerciseGroups, setExerciseGroups] = useState([]);
/* Used to store a list of the exercises that can be included in the workout */
const [exercises, setExercises] = useState(undefined);
/* Used to store the selected exercise in the 'Add new exercise' modal */
const [selectedExercise, setSelectedExercise] = useState({});
useEffect(() => {
const getExercises = async () => {
const q = query(exercisesCollectionRef, orderBy('name'));
const data = await getDocs(q);
setExercises(data.docs.map((d) => ({ ...d.data(), id: d.id })));
};
if (!isFirstLoad.current) {
getExercises();
isFirstLoad.current = true;
}
}, []);
/* Keeps track of which index the exercise is in the workout */
const index = useRef(0);
const updateExercises = (ex) => {
setExerciseGroups(exerciseGroups.concat(ex));
setSelectedExercise({});
};
const deleteExercise = (toDelete) => {
const newGroups = exerciseGroups.filter((ex) => ex.index !== toDelete);
for (let i = 0; i < exerciseGroups.length - 1; i += 1) {
newGroups[i].index = i;
}
setExerciseGroups(newGroups);
index.current -= 1;
};
/* Handles state for the add exercise modal */
const [isAddExerciseModalOpen, setAddExerciseModalOpen] = useState(false);
const handleAddExerciseModalOpen = () => setAddExerciseModalOpen(true);
const handleAddExerciseModalClose = () => {
/* This check is to differentiate between cancelling or actually adding an exercise */
if (selectedExercise.id) {
updateExercises({
...selectedExercise,
index: index.current,
sets: 0,
reps: 0,
});
index.current += 1;
}
setAddExerciseModalOpen(false);
};
/* Handles state for the editing sets/reps for an exercise modal */
const [isEditExerciseModalOpen, setEditExerciseModalOpen] = useState(false);
const handleEditExerciseModalOpen = (ex) => {
setSelectedExercise(ex);
setEditExerciseModalOpen(true);
};
const handleEditExerciseModalClose = () => {
for (let i = 0; i < exerciseGroups.length; i += 1) {
if (exerciseGroups[i].id === selectedExercise.id) {
exerciseGroups[i].sets = selectedExercise.sets;
exerciseGroups[i].reps = selectedExercise.reps;
break;
}
}
setExerciseGroups(exerciseGroups);
setEditExerciseModalOpen(false);
};
/* Transforms an exercise into the correct data structure for the form submission */
const getRepsSetsMuscles = () => {
const exercisesList = [];
const muscleGroupsList = [];
for (let i = 0; i < exerciseGroups.length; i += 1) {
/* Not sure why, but I can't seem to access the data directly here.
* So this is a workaround.
*/
const obj = { ...exerciseGroups[i] };
delete obj.instructions;
delete obj.equipment;
delete obj.videoURL;
delete obj.index;
delete obj.muscleGroups;
/* Get the muscle groups from the exercises */
if (exercises) {
for (let j = 0; j < exercises.length; j += 1) {
if (exercises[j].id === obj.id) {
for (let k = 0; k < exercises[j].muscleGroups.length; k += 1) {
if (!muscleGroupsList.includes(exercises[j].muscleGroups[k])) {
muscleGroupsList.push(exercises[j].muscleGroups[k]);
}
}
break;
}
}
}
exercisesList.push(obj);
}
return [exercisesList, muscleGroupsList.sort()];
};
/* Handles the submission of forms. */
const handleSubmit = async (event) => {
/* Prevent automatic submission and refreshing of the page. */
event.preventDefault();
const [exercisesList, muscleGroups] = getRepsSetsMuscles(event.target);
const data = {
name: event.target.workoutName.value,
imgSrc: event.target.workoutImgSrc.value,
imgAlt: event.target.workoutImgAlt.value,
muscleGroups,
exercises: exercisesList,
id: `${authUser.uid}-${event.target.workoutName.value}`,
};
/* Send the form data to the API and get a response */
const response = await fetch('/api/userworkout', {
body: JSON.stringify(data),
headers: {
'Content-Type': 'application/json',
},
method: 'POST',
});
/* Get the response, add the document, create an alert, then redirect. */
const result = await response.json();
if (result.error) {
handleAlertOpen({
heading: 'Error',
body: result.error,
variant: 'danger',
});
return;
}
if (authUser) {
if (authUser.createdWorkouts === undefined) {
authUser.createdWorkouts = [result.data];
} else {
authUser.createdWorkouts.push(result.data);
}
updateDoc(doc(db, 'users', authUser.uid), {
createdWorkouts: authUser.createdWorkouts,
})
.then(() => {
handleAlertOpen({
heading: 'Success!',
body: `${result.data.name} was added to your workout list. Redirecting...`,
variant: 'success',
});
setTimeout(() => {
router.push('/userworkouts');
}, 3000);
})
.catch((error) => {
handleAlertOpen({
heading: 'Error',
body: `${error.name}: ${error.code}`,
variant: 'danger',
});
});
}
};
const displayAlert = ({ heading, body, variant }) => {
// TODO: Check if dismissible on error
if (heading && body && variant) {
return (
<CustomAlert
heading={heading}
body={body}
variant={variant}
onClose={handleAlertClose}
/>
);
}
return null;
};
/* Handles the submission of forms to edit sets/reps of an exercise */
const handleEdit = (event) => {
event.preventDefault();
selectedExercise.sets = event.target.newsets.value;
selectedExercise.reps = event.target.newreps.value;
handleEditExerciseModalClose();
};
return (
<div className={styles.form}>
<div>
<h2>Creating new user workout</h2>
</div>
<Form onSubmit={handleSubmit} action="/api/userworkout" method="post">
<div className="mt-3 mb-3">
<Form.Label>Enter workout name:</Form.Label>
<Form.Control
id="workoutName"
type="text"
placeholder="Enter workout name"
/>
</div>
<Form.Group className="mb-3">
<Form.Label>Enter image URL to display</Form.Label>
<Form.Control
id="workoutImgSrc"
type="url"
placeholder="Enter image URL"
/>
</Form.Group>
<Form.Group className="mb-3">
<Form.Label>Enter image alt</Form.Label>
<Form.Control
id="workoutImgAlt"
type="text"
placeholder="Enter image alt (in case the image doesn't load)"
/>
</Form.Group>
<Form.Group>
<Form.Label>Exercises in this workout:</Form.Label>
<div className={styles.formexercises}>
{exerciseGroups.length === 0 ? (
<p>None</p>
) : (
exerciseGroups.map((ex) => (
<ExerciseElement
exercise={ex}
key={ex.id}
onClick={() => {
handleEditExerciseModalOpen(ex);
}}
/>
))
)}
</div>
</Form.Group>
<div className={styles.addexercise}>
<Button
variant="outline-primary"
onClick={handleAddExerciseModalOpen}
className="mt-3 mb-3"
>
+ Add an exercise
</Button>
</div>
{displayAlert(isAlertActive)}
<div className={`mt-3 ${styles.buttongroup}`}>
<Link href="/userworkouts" passHref>
<Button variant="secondary" size="lg">
Cancel
</Button>
</Link>
<Button variant="primary" type="submit" size="lg">
Submit
</Button>
</div>
</Form>
<AddExerciseModal
show={isAddExerciseModalOpen}
onClose={handleAddExerciseModalClose}
list={exercises}
setSelectedExercise={setSelectedExercise}
/>
<EditExerciseModal
show={isEditExerciseModalOpen}
onClose={handleEditExerciseModalClose}
exercise={selectedExercise}
onSubmit={handleEdit}
onDelete={() => {
deleteExercise(selectedExercise.index);
setEditExerciseModalOpen(false);
}}
/>
</div>
);
}
export default function CreateWorkout() {
return (
<>
<TopNavbar />
<div className={styles.main}>
<WorkoutForm />
</div>
</>
);
}

View File

@ -1,396 +0,0 @@
// React
import React, { useEffect, useRef, useState } from 'react';
// Next components
import Link from 'next/link';
import { useRouter } from 'next/router';
// Bootstrap components
import Button from 'react-bootstrap/Button';
import Form from 'react-bootstrap/Form';
// Firebase
import {
collection,
updateDoc,
query,
orderBy,
getDocs,
doc,
} from 'firebase/firestore';
import { db } from '../../../firebase-config';
// Custom components
import AddExerciseModal from '../../../components/WorkoutForms/AddExerciseModal';
import CustomAlert from '../../../components/EditButton/CustomAlert';
import EditExerciseModal from '../../../components/WorkoutForms/EditExerciseModal';
import ExerciseElement from '../../../components/WorkoutForms/ExerciseElement';
import TopNavbar from '../../../components/Navbar/Navbar';
// Styles
import styles from '../../../styles/WorkoutForms/WorkoutForm.module.css';
// Authentication
import { useAuth } from '../../../context/authUserContext';
// Get reference to exercises and workouts collections
const exercisesCollectionRef = collection(db, 'exercises');
function WorkoutForm() {
const router = useRouter();
const { id } = router.query;
const { authUser } = useAuth();
/* Ensures that the database is only queried once for data */
const isFirstLoad = useRef(false);
/* Handles state for validation of form */
// TODO: Form validation
// const [validated, setValidated] = useState(false);
/* Handles state for the alert */
const [isAlertActive, setAlertActive] = useState({});
const handleAlertOpen = ({ heading, body, variant }) => {
setAlertActive({ heading, body, variant });
};
const handleAlertClose = () => {
setAlertActive({});
};
/* Used to store the ids of exercises that have been added to the workout. */
const [exerciseGroups, setExerciseGroups] = useState([]);
/* Used to store a list of the exercises that can be included in the workout */
const [exercises, setExercises] = useState(undefined);
/* Stores the current workout being edited. */
const [workout, setWorkout] = useState({});
/* Used to store the selected exercise in the 'Add new exercise' modal */
const [selectedExercise, setSelectedExercise] = useState({});
/* Keeps track of which index the exercise is in the workout */
const index = useRef(0);
useEffect(() => {
const getExercises = async () => {
const q = query(exercisesCollectionRef, orderBy('name'));
const data = await getDocs(q);
setExercises(data.docs.map((d) => ({ ...d.data(), id: d.id })));
};
const getWorkout = () => {
if (authUser) {
for (let i = 0; i < authUser.createdWorkouts.length; i += 1) {
if (authUser.createdWorkouts[i].id === id) {
setWorkout(authUser.createdWorkouts[i]);
break;
}
}
}
};
const loadExerciseGroups = () => {
const newGroups = [];
if (workout.exercises) {
for (let i = 0; i < workout.exercises.length; i += 1) {
newGroups.push({ ...workout.exercises[i], index: i });
}
index.current = workout.exercises.length;
}
setExerciseGroups(newGroups);
};
if (!isFirstLoad.current) {
getExercises();
isFirstLoad.current = true;
}
if (isFirstLoad.current) {
getWorkout();
loadExerciseGroups();
}
}, [authUser, id, workout.exercises]);
const updateExercises = (ex) => {
setExerciseGroups(exerciseGroups.concat(ex));
setSelectedExercise({});
};
const deleteExercise = (toDelete) => {
const newGroups = exerciseGroups.filter((ex) => ex.index !== toDelete);
for (let i = 0; i < exerciseGroups.length - 1; i += 1) {
newGroups[i].index = i;
}
setExerciseGroups(newGroups);
index.current -= 1;
};
/* Handles state for the add exercise modal */
const [isAddExerciseModalOpen, setAddExerciseModalOpen] = useState(false);
const handleAddExerciseModalOpen = () => setAddExerciseModalOpen(true);
const handleAddExerciseModalClose = () => {
/* This check is to differentiate between cancelling or actually adding an exercise */
if (selectedExercise.id) {
updateExercises({
...selectedExercise,
index: index.current,
sets: 0,
reps: 0,
});
index.current += 1;
}
setAddExerciseModalOpen(false);
};
/* Handles state for the editing sets/reps for an exercise modal */
const [isEditExerciseModalOpen, setEditExerciseModalOpen] = useState(false);
const handleEditExerciseModalOpen = (ex) => {
setSelectedExercise(ex);
setEditExerciseModalOpen(true);
};
const handleEditExerciseModalClose = () => {
for (let i = 0; i < exerciseGroups.length; i += 1) {
if (exerciseGroups[i].id === selectedExercise.id) {
exerciseGroups[i].sets = selectedExercise.sets;
exerciseGroups[i].reps = selectedExercise.reps;
break;
}
}
setExerciseGroups(exerciseGroups);
setEditExerciseModalOpen(false);
};
/* Transforms an exercise into the correct data structure for the form submission */
const getRepsSetsMuscles = () => {
const exercisesList = [];
const muscleGroupsList = [];
for (let i = 0; i < exerciseGroups.length; i += 1) {
/* Not sure why, but I can't seem to access the data directly here.
* So this is a workaround.
*/
const obj = { ...exerciseGroups[i] };
delete obj.instructions;
delete obj.equipment;
delete obj.videoURL;
delete obj.index;
delete obj.muscleGroups;
/* Get the muscle groups from the exercises */
if (exercises) {
for (let j = 0; j < exercises.length; j += 1) {
if (exercises[j].id === obj.id) {
for (let k = 0; k < exercises[j].muscleGroups.length; k += 1) {
if (!muscleGroupsList.includes(exercises[j].muscleGroups[k])) {
muscleGroupsList.push(exercises[j].muscleGroups[k]);
}
}
break;
}
}
}
exercisesList.push(obj);
}
return [exercisesList, muscleGroupsList.sort()];
};
/* Handles the submission of forms. */
const handleSubmit = async (event) => {
/* Prevent automatic submission and refreshing of the page. */
event.preventDefault();
const [exercisesList, muscleGroups] = getRepsSetsMuscles();
const data = {
name: event.target.workoutName.value,
imgSrc: event.target.workoutImgSrc.value,
imgAlt: event.target.workoutImgAlt.value,
muscleGroups,
exercises: exercisesList,
id: `${authUser.uid}-${event.target.workoutName.value}`,
};
/* Send the form data to the API and get a response */
const response = await fetch('/api/userworkout', {
body: JSON.stringify(data),
headers: {
'Content-Type': 'application/json',
},
method: 'POST',
});
/* Get the response, add the document, create an alert, then redirect. */
const result = await response.json();
if (result.error) {
handleAlertOpen({
heading: 'Error',
body: result.error,
variant: 'danger',
});
return;
}
for (let i = 0; i < authUser.createdWorkouts.length; i += 1) {
if (authUser.createdWorkouts[i].id === result.data.id) {
authUser.createdWorkouts[i] = { ...result.data };
break;
}
}
if (authUser) {
updateDoc(doc(db, 'users', authUser.uid), {
createdWorkouts: authUser.createdWorkouts,
})
.then(() => {
handleAlertOpen({
heading: 'Success!',
body: `${result.data.name} was updated in your workout list. Redirecting...`,
variant: 'success',
});
setTimeout(() => {
router.push('/userworkouts');
}, 3000);
})
.catch((error) => {
handleAlertOpen({
heading: 'Error',
body: `${error.name}: ${error.code}`,
variant: 'danger',
});
});
}
};
const displayAlert = ({ heading, body, variant }) => {
if (heading && body && variant) {
return (
<CustomAlert
heading={heading}
body={body}
variant={variant}
onClose={handleAlertClose}
/>
);
}
return null;
};
/* Handles the submission of forms to edit sets/reps of an exercise */
const handleEdit = (event) => {
event.preventDefault();
selectedExercise.sets = event.target.newsets.value;
selectedExercise.reps = event.target.newreps.value;
handleEditExerciseModalClose();
};
return (
<div className={styles.form}>
<div>
<h2>Editing {workout.name}</h2>
</div>
<Form onSubmit={handleSubmit} action="/apiwout" method="post">
<div className="mt-3 mb-3">
<Form.Label>Enter workout name:</Form.Label>
<Form.Control
id="workoutName"
type="text"
defaultValue={workout.name}
/>
</div>
<Form.Group className="mb-3">
<Form.Label>Enter image URL to display</Form.Label>
<Form.Control
id="workoutImgSrc"
type="url"
defaultValue={workout.imgSrc}
/>
</Form.Group>
<Form.Group className="mb-3">
<Form.Label>Enter image alt</Form.Label>
<Form.Control
id="workoutImgAlt"
type="text"
defaultValue={workout.imgAlt}
/>
</Form.Group>
<Form.Group>
<Form.Label>Exercises in this workout:</Form.Label>
<div className={styles.formexercises}>
{exerciseGroups.length === 0 ? (
<p>None</p>
) : (
exerciseGroups.map((ex) => (
<ExerciseElement
exercise={ex}
key={ex.id}
onClick={() => {
handleEditExerciseModalOpen(ex);
}}
/>
))
)}
</div>
</Form.Group>
<div className={styles.addexercise}>
<Button
variant="outline-primary"
onClick={handleAddExerciseModalOpen}
className="mt-3 mb-3"
>
+ Add an exercise
</Button>
</div>
{displayAlert(isAlertActive)}
<div className={`mt-3 ${styles.buttongroup}`}>
<Link href="/userworkouts" passHref>
<Button variant="secondary" size="lg">
Cancel
</Button>
</Link>
<Button variant="primary" type="submit" size="lg">
Submit
</Button>
</div>
</Form>
<AddExerciseModal
show={isAddExerciseModalOpen}
onClose={handleAddExerciseModalClose}
list={exercises}
setSelectedExercise={setSelectedExercise}
/>
<EditExerciseModal
show={isEditExerciseModalOpen}
onClose={handleEditExerciseModalClose}
exercise={selectedExercise}
onSubmit={handleEdit}
onDelete={() => {
deleteExercise(selectedExercise.index);
setEditExerciseModalOpen(false);
}}
/>
</div>
);
}
export default function CreateWorkout() {
return (
<>
<TopNavbar />
<div className={styles.main}>
<WorkoutForm />
</div>
</>
);
}

View File

@ -1,148 +0,0 @@
// React
import React, { useEffect, useState } from 'react';
// Bootstrap components
import Col from 'react-bootstrap/Col';
import Container from 'react-bootstrap/Container';
import Row from 'react-bootstrap/Row';
import Modal from 'react-bootstrap/Modal';
import Button from 'react-bootstrap/Button';
// Next components
import Image from 'next/image';
// Custom components
import List from '../../components/List/List';
import TopNavbar from '../../components/Navbar/Navbar';
import WorkoutList from '../../components/WorkoutList/WorkoutList';
// Styles
import styles from '../../styles/Workouts/Workouts.module.css';
// Authentication
import { useAuth } from '../../context/authUserContext';
export default function WorkoutsPage({ testData }) {
const [workoutList, setWorkoutList] = useState([]);
const { authUser } = useAuth();
const [selectedWorkout, setSelectedWorkout] = useState();
const [workouts, setWorkouts] = useState([]);
/* Only render Card if innerWidth > 576px (small breakpoint) */
const [toRenderCard, setRenderCard] = useState(true);
useEffect(() => {
if (testData !== undefined) {
setWorkouts(testData);
}
if (authUser) {
const createdWorkouts =
authUser.createdWorkouts !== undefined ? authUser.createdWorkouts : [];
setWorkouts(createdWorkouts);
setSelectedWorkout(createdWorkouts[0]);
setWorkoutList(createdWorkouts);
}
if (window.innerWidth < 576) {
setRenderCard(false);
}
}, [authUser, workouts, testData]);
const [isOpen, setOpen] = useState(false);
const handleOpen = () => setOpen(true);
const handleClose = () => setOpen(false);
/* Handles the onClick events for each workout.
* The split is needed to handle both mobile/desktop views at the same time.
* If the window width is small, the modal is opened when the elements are
* clicked. Otherwise, the card is updated.
*/
const onClick = (newWorkout) => {
if (!toRenderCard) {
handleOpen();
}
setSelectedWorkout(newWorkout);
};
/* When an exercise is deleted, remove it from the list. */
const onDelete = (id) => {
setWorkoutList(workouts.filter((doc) => doc.id !== id));
};
return (
<>
<TopNavbar />
<Container className={styles.container}>
<Row>
{toRenderCard && (
<Col xs={6}>
{selectedWorkout != null && (
<div className={styles.imgcontainer}>
<Image
src={selectedWorkout.imgSrc}
alt={selectedWorkout.imgAlt}
width="100%"
height="70%"
layout="responsive"
object-fit="cover"
style={{ width: '100%', height: '48vh' }}
/>
<div className={styles.textblock}>
<h1>{selectedWorkout.name}</h1>
<p>{selectedWorkout.muscleGroups.join(', ')}</p>
</div>
</div>
)}
{selectedWorkout != null && (
<div>
<main className={styles.workoutlist}>
<WorkoutList exerciseList={selectedWorkout.exercises} />
</main>
</div>
)}
</Col>
)}
<Col>
<List
list={workoutList}
listType="radio"
selected={selectedWorkout}
setSelected={onClick}
type="user"
onDelete={onDelete}
/>
</Col>
</Row>
</Container>
{selectedWorkout !== undefined && (
<Modal show={isOpen} onHide={handleClose} centered scrollable size="lg">
<Modal.Header closeButton>
<Modal.Title>{selectedWorkout.name}</Modal.Title>
</Modal.Header>
<Modal.Body>
{selectedWorkout != null && (
<div>
<main className={styles.workoutlist}>
<WorkoutList exerciseList={selectedWorkout.exercises} />
</main>
</div>
)}
</Modal.Body>
<Modal.Footer>
<Button variant="primary" onClick={handleClose}>
Close
</Button>
</Modal.Footer>
</Modal>
)}
</>
);
}

View File

@ -284,7 +284,6 @@ function WorkoutForm() {
exerciseGroups.map((ex) => (
<ExerciseElement
exercise={ex}
key={ex.id}
onClick={() => {
handleEditExerciseModalOpen(ex);
}}

View File

@ -314,7 +314,6 @@ function WorkoutForm() {
exerciseGroups.map((ex) => (
<ExerciseElement
exercise={ex}
key={ex.id}
onClick={() => {
handleEditExerciseModalOpen(ex);
}}