Updated workout forms for compatability with database
This commit is contained in:
parent
cef2d9fd84
commit
6b748f75fe
|
@ -20,7 +20,7 @@ import { useAuth } from '../../context/authUserContext';
|
|||
// Get reference to users collection
|
||||
const usersCollectionRef = collection(db, 'users');
|
||||
|
||||
export default function Element({ element, type, onDelete }) {
|
||||
export default function Element({ element, type, onDelete, allowEditing }) {
|
||||
/* Paths of the images of the favourite button */
|
||||
const star = '/images/star.png';
|
||||
const starFilled = '/images/starFilled.png';
|
||||
|
@ -120,8 +120,7 @@ export default function Element({ element, type, onDelete }) {
|
|||
|
||||
const makeButton = () => {
|
||||
if (authUser) {
|
||||
/* Crude check for checking if the element is an exercise or workout */
|
||||
if (element.instructions !== undefined) {
|
||||
if (type === 'exercises') {
|
||||
return (
|
||||
<EditButton
|
||||
type="exercise"
|
||||
|
@ -142,33 +141,37 @@ export default function Element({ element, type, onDelete }) {
|
|||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={styles.element}>
|
||||
<Image
|
||||
src={element.imgSrc}
|
||||
alt={element.imgAlt}
|
||||
height={84}
|
||||
width={120}
|
||||
width={100}
|
||||
height={100}
|
||||
/>
|
||||
|
||||
<div className={styles.txt}>
|
||||
<h1>{element.name}</h1>
|
||||
</div>
|
||||
|
||||
<div className={styles.star}>
|
||||
<form>
|
||||
<input
|
||||
type="image"
|
||||
src={imgPath}
|
||||
height={38}
|
||||
width={38}
|
||||
alt="star"
|
||||
onClick={toggleStar}
|
||||
/>
|
||||
</form>
|
||||
</div>
|
||||
<div className={styles.buttons}>
|
||||
<div className={styles.star}>
|
||||
<form>
|
||||
<Image
|
||||
src={imgPath}
|
||||
alt="star"
|
||||
width={50}
|
||||
height={50}
|
||||
onClick={toggleStar}
|
||||
/>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div className={styles.star}>{makeButton()}</div>
|
||||
{allowEditing !== undefined && (
|
||||
<div className={styles.star}>{makeButton()}</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -19,6 +19,7 @@ import SelectedElement from './SelectedElement';
|
|||
* @param {*} setSelected The function that sets the state of selected
|
||||
* @param {*} type Either "exercises" or "workouts"
|
||||
* @param {*} onDelete The callback function to handle an element being deleted from the list.
|
||||
* @param {*} allowEditing True if the edit button should appear on the elements.
|
||||
* @returns
|
||||
*/
|
||||
export default function List({
|
||||
|
@ -28,6 +29,7 @@ export default function List({
|
|||
setSelected,
|
||||
type,
|
||||
onDelete,
|
||||
allowEditing,
|
||||
}) {
|
||||
// A function to handle when a new element is selected
|
||||
const handleChange = (e) => {
|
||||
|
@ -58,21 +60,30 @@ export default function List({
|
|||
vertical
|
||||
name="button-list"
|
||||
>
|
||||
{filteredList.map((element) => (
|
||||
<ToggleButton
|
||||
key={element.id}
|
||||
id={`${listType}-${element.id}`}
|
||||
variant="light"
|
||||
name={listType}
|
||||
value={element}
|
||||
>
|
||||
{selected === element.name ? (
|
||||
<SelectedElement element={element} type={type} />
|
||||
) : (
|
||||
<Element element={element} type={type} onDelete={onDelete} />
|
||||
)}
|
||||
</ToggleButton>
|
||||
))}
|
||||
{filteredList.length === 0 ? (
|
||||
<h3>No {type} available</h3>
|
||||
) : (
|
||||
filteredList.map((element) => (
|
||||
<ToggleButton
|
||||
key={element.id}
|
||||
id={`${listType}-${element.id}`}
|
||||
variant="light"
|
||||
name={listType}
|
||||
value={element}
|
||||
>
|
||||
{selected === element.name ? (
|
||||
<SelectedElement element={element} type={type} />
|
||||
) : (
|
||||
<Element
|
||||
element={element}
|
||||
type={type}
|
||||
onDelete={onDelete}
|
||||
allowEditing={allowEditing}
|
||||
/>
|
||||
)}
|
||||
</ToggleButton>
|
||||
))
|
||||
)}
|
||||
</ToggleButtonGroup>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -30,12 +30,12 @@ export default function WorkoutElement({ element }) {
|
|||
<Col xs={6}>
|
||||
<Row>
|
||||
<div className={styles.sr}>
|
||||
<p>{element.sets} sets</p>
|
||||
<p>{element.reps} reps</p>
|
||||
</div>
|
||||
</Row>
|
||||
<Row>
|
||||
<div className={styles.sr}>
|
||||
<p>{element.reps} reps</p>
|
||||
<p>{element.sets} sets</p>
|
||||
</div>
|
||||
</Row>
|
||||
</Col>
|
||||
|
|
|
@ -10,8 +10,17 @@ export default function handler(req, res) {
|
|||
body.imgAlt = `Diagram for how to perform a ${body.name}`;
|
||||
}
|
||||
|
||||
if (!body.name || !body.muscleGroups || !body.exercises) {
|
||||
return res.status(400).json({ data: 'Required fields not found.' });
|
||||
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 });
|
||||
|
|
|
@ -2,14 +2,18 @@
|
|||
import React, { useEffect, useRef, useState } from 'react';
|
||||
|
||||
// Next components
|
||||
import Image from 'next/image';
|
||||
import Link from 'next/link';
|
||||
import { useRouter } from 'next/router';
|
||||
|
||||
// Bootstrap components
|
||||
import Button from 'react-bootstrap/Button';
|
||||
import Container from 'react-bootstrap/Container';
|
||||
import Col from 'react-bootstrap/Col';
|
||||
import Form from 'react-bootstrap/Form';
|
||||
import Modal from 'react-bootstrap/Modal';
|
||||
import OverlayTrigger from 'react-bootstrap/OverlayTrigger';
|
||||
import Row from 'react-bootstrap/Row';
|
||||
import Tooltip from 'react-bootstrap/Tooltip';
|
||||
|
||||
// Firebase
|
||||
import {
|
||||
|
@ -18,35 +22,27 @@ import {
|
|||
query,
|
||||
orderBy,
|
||||
getDocs,
|
||||
getDoc,
|
||||
doc,
|
||||
} from 'firebase/firestore';
|
||||
import { db } from '../../../firebase-config';
|
||||
|
||||
// Custom components
|
||||
import CustomAlert from '../../../components/EditButton/CustomAlert';
|
||||
import List from '../../../components/List/List';
|
||||
import TopNavbar from '../../../components/Navbar/Navbar';
|
||||
|
||||
// Styles
|
||||
import styles from '../../../styles/EditButton.module.css';
|
||||
|
||||
// Get muscle list
|
||||
import muscles from '../../../public/muscles.json' assert { type: 'json' };
|
||||
|
||||
// Get reference to exercises and workouts collections
|
||||
const workoutsCollectionRef = collection(db, 'workouts');
|
||||
const exercisesCollectionRef = collection(db, 'exercises');
|
||||
|
||||
function WorkoutForm() {
|
||||
const router = useRouter();
|
||||
const { id } = router.query;
|
||||
|
||||
/* Ensures that the database is only queried once for data */
|
||||
const isFirstLoad = useRef(false);
|
||||
|
||||
/* Handles state for the checkboxes */
|
||||
const [checkboxes, setCheckboxes] = useState([]);
|
||||
|
||||
/* Handles state for validation of form */
|
||||
// TODO: Form validation
|
||||
// const [validated, setValidated] = useState(false);
|
||||
|
@ -60,151 +56,85 @@ function WorkoutForm() {
|
|||
setAlertActive({});
|
||||
};
|
||||
|
||||
/* Used to manage the list of chosen exercises in the form */
|
||||
const chosenExercises = useRef([]);
|
||||
|
||||
/* Used to manage the list of chosen muscle groups in the form */
|
||||
const chosenMuscleGroups = useRef([]);
|
||||
|
||||
/* Used to get a list of checkboxes for exercises to choose for a workout
|
||||
* TODO: Replace this with just a List component.
|
||||
*/
|
||||
const [exerciseOptions, setExerciseOptions] = useState([]);
|
||||
/* 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);
|
||||
};
|
||||
|
||||
const updateChosenMuscles = (ex) => {
|
||||
if (chosenMuscleGroups.current.includes(ex.target.value)) {
|
||||
const filtered = chosenMuscleGroups.current.filter(
|
||||
(i) => i !== ex.target.value
|
||||
);
|
||||
chosenMuscleGroups.current = filtered;
|
||||
} else {
|
||||
chosenMuscleGroups.current.push(ex.target.value);
|
||||
}
|
||||
};
|
||||
|
||||
const updateChosenExercises = (ex) => {
|
||||
if (chosenExercises.current.includes(ex.target.value)) {
|
||||
const filtered = chosenExercises.current.filter(
|
||||
(i) => i !== ex.target.value
|
||||
);
|
||||
chosenExercises.current = filtered;
|
||||
} else {
|
||||
chosenExercises.current.push(ex.target.value);
|
||||
}
|
||||
};
|
||||
|
||||
const makeCheckboxes = () => {
|
||||
const checkboxColumns = [];
|
||||
for (let i = 0; i < muscles.length; i += 1) {
|
||||
const { group, musclesList } = muscles[i];
|
||||
const boxes = [];
|
||||
for (let j = 0; j < musclesList.length; j += 1) {
|
||||
const { mId, name } = musclesList[j];
|
||||
boxes.push(
|
||||
<div className="mb-3" key={mId}>
|
||||
<Form.Check
|
||||
type="checkbox"
|
||||
id="workoutMuscleGroups"
|
||||
value={name}
|
||||
label={name}
|
||||
key={name}
|
||||
onChange={updateChosenMuscles}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
/* TODO: This is still giving unique key errors, not sure why. */
|
||||
checkboxColumns.push(
|
||||
<Col key={group}>
|
||||
<b>{group}</b>
|
||||
{boxes}
|
||||
</Col>
|
||||
);
|
||||
}
|
||||
return checkboxColumns;
|
||||
};
|
||||
|
||||
const makeExerciseOptions = () => {
|
||||
if (exercises !== undefined) {
|
||||
return exercises.docs.map((document) => (
|
||||
<div className="mb-3" key={document.id}>
|
||||
<Row>
|
||||
<Col xs={4}>
|
||||
<Form.Control placeholder={document.data().name} readOnly />
|
||||
</Col>
|
||||
<Col xs={2}>
|
||||
<Form.Control
|
||||
placeholder="Reps"
|
||||
onChange={updateChosenExercises}
|
||||
/>
|
||||
</Col>
|
||||
<Col xs={2}>
|
||||
<Form.Control
|
||||
placeholder="Sets"
|
||||
onChange={updateChosenExercises}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
{/* <Form.Check
|
||||
type="checkbox"
|
||||
id="chosenOptions"
|
||||
value={document.id}
|
||||
label={document.data().name}
|
||||
key={document.data().name}
|
||||
onChange={updateChosenExercises}
|
||||
/> */}
|
||||
</div>
|
||||
));
|
||||
}
|
||||
return null;
|
||||
setExercises(data.docs.map((d) => ({ ...d.data(), id: d.id })));
|
||||
};
|
||||
|
||||
if (!isFirstLoad.current) {
|
||||
getExercises();
|
||||
isFirstLoad.current = true;
|
||||
}
|
||||
}, []);
|
||||
|
||||
if (isFirstLoad.current) {
|
||||
setCheckboxes(makeCheckboxes());
|
||||
setExerciseOptions(makeExerciseOptions());
|
||||
/* 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;
|
||||
}
|
||||
}, [exercises]);
|
||||
setExerciseGroups(newGroups);
|
||||
index.current -= 1;
|
||||
};
|
||||
|
||||
/* Handles state for the add exercise modal */
|
||||
const [isModalOpen, setModalOpen] = useState(false);
|
||||
const handleModalOpen = () => setModalOpen(true);
|
||||
const handleModalClose = () => {
|
||||
/* This check is to differentiate between cancelling or actually adding an exercise */
|
||||
if (selectedExercise.id) {
|
||||
updateExercises({ ...selectedExercise, index: index.current });
|
||||
index.current += 1;
|
||||
}
|
||||
setModalOpen(false);
|
||||
};
|
||||
|
||||
/* Transforms an exercise into the correct data structure for the form submission */
|
||||
const getExercisesFromId = async () => {
|
||||
const promiseList = [];
|
||||
for (let i = 0; i < chosenExercises.current.length; i += 1) {
|
||||
promiseList.push(
|
||||
getDoc(doc(db, 'exercises', chosenExercises.current[i]))
|
||||
);
|
||||
}
|
||||
const exerciseList = await Promise.all(promiseList);
|
||||
const list = [];
|
||||
const getRepsSetsMuscles = (values) => {
|
||||
const exercisesList = [];
|
||||
const muscleGroupsList = [];
|
||||
|
||||
for (let i = 0; i < exerciseList.length; i += 1) {
|
||||
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 = { ...exerciseList[i].data() };
|
||||
const obj = { ...exerciseGroups[i] };
|
||||
delete obj.instructions;
|
||||
delete obj.equipment;
|
||||
delete obj.muscleGroups;
|
||||
delete obj.videoURL;
|
||||
list.push(obj);
|
||||
// Add sets and reps to this
|
||||
|
||||
/* Get the muscle groups from the exercise */
|
||||
for (let j = 0; j < obj.muscleGroups.length; j += 1) {
|
||||
if (!muscleGroupsList.includes(obj.muscleGroups[j])) {
|
||||
muscleGroupsList.push(obj.muscleGroups[j]);
|
||||
}
|
||||
}
|
||||
delete obj.muscleGroups;
|
||||
|
||||
obj.reps = Number(values[`${obj.id}-reps`].value);
|
||||
obj.sets = Number(values[`${obj.id}-sets`].value);
|
||||
exercisesList.push(obj);
|
||||
}
|
||||
|
||||
return list;
|
||||
return [exercisesList, muscleGroupsList.sort()];
|
||||
};
|
||||
|
||||
/* Handles the submission of forms. */
|
||||
|
@ -212,16 +142,15 @@ function WorkoutForm() {
|
|||
/* Prevent automatic submission and refreshing of the page. */
|
||||
event.preventDefault();
|
||||
|
||||
const exercisesList = await getExercisesFromId();
|
||||
const [exercisesList, muscleGroups] = getRepsSetsMuscles(event.target);
|
||||
|
||||
/* TODO: Implement muscleGroups and image uploading */
|
||||
const data = {
|
||||
name: event.target.workoutName.value,
|
||||
imgSrc: '/images/push-ups.png',
|
||||
imgAlt: `Picture of ${event.target.workoutName.value}`,
|
||||
muscleGroups: chosenMuscleGroups.current,
|
||||
muscleGroups,
|
||||
exercises: exercisesList,
|
||||
id,
|
||||
};
|
||||
|
||||
/* Send the form data to the API and get a response */
|
||||
|
@ -235,6 +164,15 @@ function WorkoutForm() {
|
|||
|
||||
/* 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;
|
||||
}
|
||||
|
||||
addDoc(workoutsCollectionRef, result.data)
|
||||
.then(() => {
|
||||
handleAlertOpen({
|
||||
|
@ -272,24 +210,24 @@ function WorkoutForm() {
|
|||
|
||||
return (
|
||||
<div className={styles.form}>
|
||||
<h2>Creating new workout</h2>
|
||||
<div style={{ padding: '0 10vw' }}>
|
||||
<h2>Creating new workout</h2>
|
||||
</div>
|
||||
|
||||
<Form onSubmit={handleSubmit} action="/api/workout" method="post">
|
||||
<Form.Group>
|
||||
<Form.Label>Workout name</Form.Label>
|
||||
<Form
|
||||
onSubmit={handleSubmit}
|
||||
action="/api/workout"
|
||||
method="post"
|
||||
className={styles.form}
|
||||
>
|
||||
<div className={styles.formname}>
|
||||
<Form.Label>Enter workout name:</Form.Label>
|
||||
<Form.Control
|
||||
id="workoutName"
|
||||
type="text"
|
||||
placeholder="Enter workout name"
|
||||
/>
|
||||
</Form.Group>
|
||||
|
||||
<Form.Group>
|
||||
<Form.Label>Select targeted areas</Form.Label>
|
||||
<Container fluid>
|
||||
<Row>{checkboxes}</Row>
|
||||
</Container>
|
||||
</Form.Group>
|
||||
</div>
|
||||
|
||||
{/* Not going to work just yet */}
|
||||
{/* <Form.Group controlId="formThumbnail">
|
||||
|
@ -303,16 +241,101 @@ function WorkoutForm() {
|
|||
</Form.Group> */}
|
||||
|
||||
<Form.Group>
|
||||
<Form.Label>Select exercises to include</Form.Label>
|
||||
{exerciseOptions}
|
||||
<Form.Label>Exercises in this workout:</Form.Label>
|
||||
<div className={styles.formexercises}>
|
||||
{exerciseGroups.length === 0 ? (
|
||||
<p>None</p>
|
||||
) : (
|
||||
exerciseGroups.map((ex) => (
|
||||
<Row className="mb-3" key={ex.index}>
|
||||
<Col xs={5} className="mt-2">
|
||||
{ex.name}
|
||||
</Col>
|
||||
|
||||
<Col xs={1} className="mt-2">
|
||||
Reps
|
||||
</Col>
|
||||
|
||||
<Col xs={2}>
|
||||
<Form.Control id={`${ex.id}-reps`} />
|
||||
</Col>
|
||||
|
||||
<Col xs={1} className="mt-2">
|
||||
Sets
|
||||
</Col>
|
||||
|
||||
<Col xs={2}>
|
||||
<Form.Control id={`${ex.id}-sets`} />
|
||||
</Col>
|
||||
|
||||
<Col xs={1} className="mt-2">
|
||||
{/* TODO: Make this tooltip appear next to the image */}
|
||||
<OverlayTrigger
|
||||
overlay={<Tooltip>Delete {ex.name}</Tooltip>}
|
||||
>
|
||||
{({ ref }) => (
|
||||
<Image
|
||||
src="/images/delete.svg"
|
||||
alt={`Delete ${ex.name}`}
|
||||
height={20}
|
||||
width={20}
|
||||
onClick={() => deleteExercise(ex.index)}
|
||||
lazyRoot={ref}
|
||||
/>
|
||||
)}
|
||||
</OverlayTrigger>
|
||||
</Col>
|
||||
</Row>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</Form.Group>
|
||||
|
||||
<Button variant="primary" type="submit">
|
||||
Submit
|
||||
</Button>
|
||||
<div className={styles.formbuttons}>
|
||||
<Button variant="primary" onClick={handleModalOpen}>
|
||||
Add an exercise
|
||||
</Button>
|
||||
<div>
|
||||
<Link href="/workouts" passHref>
|
||||
<Button variant="secondary">Cancel</Button>
|
||||
</Link>{' '}
|
||||
<Button variant="primary" type="submit">
|
||||
Submit
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Form>
|
||||
|
||||
{displayAlert(isAlertActive)}
|
||||
|
||||
<Modal show={isModalOpen} onHide={handleModalClose} centered size="lg">
|
||||
<Modal.Header>
|
||||
<Modal.Title>
|
||||
<p>Add an exercise</p>
|
||||
</Modal.Title>
|
||||
</Modal.Header>
|
||||
|
||||
<Modal.Body>
|
||||
{exercises && (
|
||||
<List
|
||||
list={exercises}
|
||||
listType="radio"
|
||||
type="exercises"
|
||||
setSelected={setSelectedExercise}
|
||||
/>
|
||||
)}
|
||||
</Modal.Body>
|
||||
|
||||
<Modal.Footer>
|
||||
<Button variant="secondary" onClick={handleModalClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
|
||||
<Button variant="primary" onClick={handleModalClose}>
|
||||
Add
|
||||
</Button>
|
||||
</Modal.Footer>
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -2,37 +2,39 @@
|
|||
import React, { useEffect, useRef, useState } from 'react';
|
||||
|
||||
// Next components
|
||||
import Image from 'next/image';
|
||||
import Link from 'next/link';
|
||||
import { useRouter } from 'next/router';
|
||||
|
||||
// Bootstrap components
|
||||
import Button from 'react-bootstrap/Button';
|
||||
import Container from 'react-bootstrap/Container';
|
||||
import Col from 'react-bootstrap/Col';
|
||||
import Form from 'react-bootstrap/Form';
|
||||
import Modal from 'react-bootstrap/Modal';
|
||||
import OverlayTrigger from 'react-bootstrap/OverlayTrigger';
|
||||
import Row from 'react-bootstrap/Row';
|
||||
import Tooltip from 'react-bootstrap/Tooltip';
|
||||
|
||||
// Firebase
|
||||
import {
|
||||
getDoc,
|
||||
collection,
|
||||
updateDoc,
|
||||
doc,
|
||||
query,
|
||||
orderBy,
|
||||
getDocs,
|
||||
getDoc,
|
||||
doc,
|
||||
} from 'firebase/firestore';
|
||||
import { db } from '../../../firebase-config';
|
||||
|
||||
// Custom components
|
||||
import CustomAlert from '../../../components/EditButton/CustomAlert';
|
||||
import List from '../../../components/List/List';
|
||||
import TopNavbar from '../../../components/Navbar/Navbar';
|
||||
|
||||
// Styles
|
||||
import styles from '../../../styles/EditButton.module.css';
|
||||
|
||||
// Get muscle list
|
||||
import muscles from '../../../public/muscles.json' assert { type: 'json' };
|
||||
|
||||
// Get reference to exercises and workouts collections
|
||||
const exercisesCollectionRef = collection(db, 'exercises');
|
||||
|
||||
|
@ -40,15 +42,9 @@ function WorkoutForm() {
|
|||
const router = useRouter();
|
||||
const { id } = router.query;
|
||||
|
||||
/* Ensures that the Firestore is only contacted once for data */
|
||||
/* Ensures that the database is only queried once for data */
|
||||
const isFirstLoad = useRef(false);
|
||||
|
||||
/* Handles state for the workout */
|
||||
const [workout, setWorkout] = useState({});
|
||||
|
||||
/* Handles state for the checkboxes */
|
||||
const [checkboxes, setCheckboxes] = useState([]);
|
||||
|
||||
/* Handles state for validation of form */
|
||||
// TODO: Form validation
|
||||
// const [validated, setValidated] = useState(false);
|
||||
|
@ -62,134 +58,38 @@ function WorkoutForm() {
|
|||
setAlertActive({});
|
||||
};
|
||||
|
||||
/* Used to manage the list of chosen exercises in the form */
|
||||
const chosenExercises = useRef([]);
|
||||
/* Used to store the ids of exercises that have been added to the workout. */
|
||||
const [exerciseGroups, setExerciseGroups] = useState([]);
|
||||
|
||||
/* Used to manage the list of chosen muscle groups in the form */
|
||||
const chosenMuscleGroups = useRef([]);
|
||||
|
||||
/* Used to get a list of checkboxes for exercises to choose for a workout */
|
||||
const [exerciseOptions, setExerciseOptions] = useState([]);
|
||||
|
||||
/* Used to ensure that the exercises collection is queried only once */
|
||||
/* Used to store a list of the exercises that can be included in the workout */
|
||||
const [exercises, setExercises] = useState(undefined);
|
||||
|
||||
useEffect(() => {
|
||||
const getWorkout = async () => {
|
||||
const workoutDoc = await getDoc(doc(db, 'workouts', id));
|
||||
chosenExercises.current = workoutDoc.data().exercises;
|
||||
chosenMuscleGroups.current = workoutDoc.data().muscleGroups;
|
||||
setWorkout(workoutDoc.data());
|
||||
};
|
||||
/* 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({});
|
||||
|
||||
useEffect(() => {
|
||||
const getExercises = async () => {
|
||||
const q = query(exercisesCollectionRef, orderBy('name'));
|
||||
const data = await getDocs(q);
|
||||
setExercises(data);
|
||||
setExercises(data.docs.map((d) => ({ ...d.data(), id: d.id })));
|
||||
};
|
||||
|
||||
const updateChosenMuscles = (ex) => {
|
||||
if (chosenMuscleGroups.current.includes(ex.target.value)) {
|
||||
const filtered = chosenMuscleGroups.current.filter(
|
||||
(i) => i !== ex.target.value
|
||||
);
|
||||
chosenMuscleGroups.current = filtered;
|
||||
} else {
|
||||
chosenMuscleGroups.current.push(ex.target.value);
|
||||
}
|
||||
const getWorkout = async () => {
|
||||
const workoutDoc = await getDoc(doc(db, 'workouts', id));
|
||||
setWorkout(workoutDoc.data());
|
||||
};
|
||||
|
||||
const updateChosenExercises = (ex) => {
|
||||
if (chosenExercises.current.includes(ex.target.value)) {
|
||||
const filtered = chosenExercises.current.filter(
|
||||
(i) => i !== ex.target.value
|
||||
);
|
||||
chosenExercises.current = filtered;
|
||||
} else {
|
||||
chosenExercises.current.push(ex.target.value);
|
||||
}
|
||||
};
|
||||
|
||||
const makeCheckboxes = () => {
|
||||
const checkboxColumns = [];
|
||||
// Create checkboxes for all muscles, and pre-check boxes
|
||||
const preChecked =
|
||||
workout.muscleGroups !== undefined ? workout.muscleGroups : [];
|
||||
for (let i = 0; i < muscles.length; i += 1) {
|
||||
const { group, musclesList } = muscles[i];
|
||||
const boxes = [];
|
||||
for (let j = 0; j < musclesList.length; j += 1) {
|
||||
const { mId, name } = musclesList[j];
|
||||
if (preChecked.includes(name)) {
|
||||
boxes.push(
|
||||
<div className="mb-3" key={mId}>
|
||||
<Form.Check
|
||||
type="checkbox"
|
||||
id="workoutMuscleGroups"
|
||||
value={name}
|
||||
label={name}
|
||||
key={name}
|
||||
defaultChecked
|
||||
onChange={updateChosenMuscles}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
boxes.push(
|
||||
<div className="mb-3" key={mId}>
|
||||
<Form.Check
|
||||
type="checkbox"
|
||||
id="workoutMuscleGroups"
|
||||
value={name}
|
||||
label={name}
|
||||
key={name}
|
||||
onChange={updateChosenMuscles}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
const loadExerciseGroups = () => {
|
||||
const newGroups = [];
|
||||
if (workout.exercises) {
|
||||
for (let i = 0; i < workout.exercises.length; i += 1) {
|
||||
newGroups.push({ ...workout.exercises[i], index: i });
|
||||
}
|
||||
/* TODO: This is still giving unique key errors, not sure why. */
|
||||
checkboxColumns.push(
|
||||
<Col key={group}>
|
||||
<b>{group}</b>
|
||||
{boxes}
|
||||
</Col>
|
||||
);
|
||||
}
|
||||
return checkboxColumns;
|
||||
};
|
||||
|
||||
const makeExerciseOptions = () => {
|
||||
if (exercises !== undefined) {
|
||||
const preChecked =
|
||||
workout.exercises !== undefined ? workout.exercises : [];
|
||||
return exercises.docs.map((document) => (
|
||||
<div className="mb-3" key={document.id}>
|
||||
{preChecked.includes(document.id) ? (
|
||||
<Form.Check
|
||||
type="checkbox"
|
||||
id="chosenOptions"
|
||||
value={document.id}
|
||||
label={document.data().name}
|
||||
key={document.data().name}
|
||||
onChange={updateChosenExercises}
|
||||
defaultChecked
|
||||
/>
|
||||
) : (
|
||||
<Form.Check
|
||||
type="checkbox"
|
||||
id="chosenOptions"
|
||||
value={document.id}
|
||||
label={document.data().name}
|
||||
key={document.data().name}
|
||||
onChange={updateChosenExercises}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
));
|
||||
}
|
||||
return null;
|
||||
setExerciseGroups(newGroups);
|
||||
};
|
||||
|
||||
if (!isFirstLoad.current) {
|
||||
|
@ -199,23 +99,82 @@ function WorkoutForm() {
|
|||
}
|
||||
|
||||
if (isFirstLoad.current) {
|
||||
setCheckboxes(makeCheckboxes());
|
||||
setExerciseOptions(makeExerciseOptions());
|
||||
loadExerciseGroups();
|
||||
}
|
||||
}, [exercises, id, workout.exercises, workout.muscleGroups]);
|
||||
}, [id, workout.exercises]);
|
||||
|
||||
/* 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 [isModalOpen, setModalOpen] = useState(false);
|
||||
const handleModalOpen = () => setModalOpen(true);
|
||||
const handleModalClose = () => {
|
||||
/* This check is to differentiate between cancelling or actually adding an exercise */
|
||||
if (selectedExercise.id) {
|
||||
updateExercises({ ...selectedExercise, index: index.current });
|
||||
index.current += 1;
|
||||
}
|
||||
setModalOpen(false);
|
||||
};
|
||||
|
||||
/* Transforms an exercise into the correct data structure for the form submission */
|
||||
const getRepsSetsMuscles = (values) => {
|
||||
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;
|
||||
|
||||
/* Get the muscle groups from the exercise */
|
||||
for (let j = 0; j < obj.muscleGroups.length; j += 1) {
|
||||
if (!muscleGroupsList.includes(obj.muscleGroups[j])) {
|
||||
muscleGroupsList.push(obj.muscleGroups[j]);
|
||||
}
|
||||
}
|
||||
|
||||
obj.reps = Number(values[`${obj.id}-reps`].value);
|
||||
obj.sets = Number(values[`${obj.id}-sets`].value);
|
||||
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);
|
||||
|
||||
/* TODO: Implement muscleGroups and image uploading */
|
||||
const data = {
|
||||
name: event.target.workoutName.value,
|
||||
imgSrc: '/images/push-ups.png',
|
||||
imgAlt: `Picture of ${event.target.workoutName.value}`,
|
||||
muscleGroups: chosenMuscleGroups.current,
|
||||
exercises: chosenExercises.current,
|
||||
muscleGroups,
|
||||
exercises: exercisesList,
|
||||
id,
|
||||
};
|
||||
|
||||
|
@ -228,13 +187,22 @@ function WorkoutForm() {
|
|||
method: 'POST',
|
||||
});
|
||||
|
||||
/* Get the response, update the document, create an alert, then redirect. */
|
||||
/* Get the response, add the document, create an alert, then redirect. */
|
||||
const result = await response.json();
|
||||
updateDoc(doc(db, 'workouts', id), result.data)
|
||||
if (result.error) {
|
||||
handleAlertOpen({
|
||||
heading: 'Error',
|
||||
body: result.error,
|
||||
variant: 'danger',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
updateDoc(doc(db, 'exercises', id), result.data)
|
||||
.then(() => {
|
||||
handleAlertOpen({
|
||||
heading: 'Success!',
|
||||
body: `${result.data.name} was updated in the workout list. Redirecting...`,
|
||||
body: `${result.data.name} was added to the workout list. Redirecting...`,
|
||||
variant: 'success',
|
||||
});
|
||||
setTimeout(() => {
|
||||
|
@ -267,24 +235,24 @@ function WorkoutForm() {
|
|||
|
||||
return (
|
||||
<div className={styles.form}>
|
||||
<h2>{`Editing '${workout.name}'`}</h2>
|
||||
<div style={{ padding: '0 10vw' }}>
|
||||
<h2>Editing {workout.name}</h2>
|
||||
</div>
|
||||
|
||||
<Form onSubmit={handleSubmit} action="/api/workout" method="post">
|
||||
<Form.Group>
|
||||
<Form.Label>Workout name</Form.Label>
|
||||
<Form
|
||||
onSubmit={handleSubmit}
|
||||
action="/api/workout"
|
||||
method="post"
|
||||
className={styles.form}
|
||||
>
|
||||
<div className={styles.formname}>
|
||||
<Form.Label>Enter workout name:</Form.Label>
|
||||
<Form.Control
|
||||
id="workoutName"
|
||||
type="text"
|
||||
defaultValue={workout.name}
|
||||
/>
|
||||
</Form.Group>
|
||||
|
||||
<Form.Group>
|
||||
<Form.Label>Select targeted areas</Form.Label>
|
||||
<Container fluid>
|
||||
<Row>{checkboxes}</Row>
|
||||
</Container>
|
||||
</Form.Group>
|
||||
</div>
|
||||
|
||||
{/* Not going to work just yet */}
|
||||
{/* <Form.Group controlId="formThumbnail">
|
||||
|
@ -292,27 +260,112 @@ function WorkoutForm() {
|
|||
<Form.Control type="file" />
|
||||
</Form.Group>
|
||||
|
||||
<Form.Group controlId="formimgAlt">
|
||||
<Form.Group controlId="formImgAlt">
|
||||
<Form.Label>Enter text to show if image doesn't load</Form.Label>
|
||||
<Form.Control type="imgAlt" defaultValue={workout.imgAlt} />
|
||||
<Form.Control type="imgAlt" placeholder="Enter image alt" />
|
||||
</Form.Group> */}
|
||||
|
||||
<Form.Group>
|
||||
<Form.Label>Select exercises to include</Form.Label>
|
||||
{exerciseOptions}
|
||||
<Form.Label>Exercises in this workout:</Form.Label>
|
||||
<div className={styles.formexercises}>
|
||||
{exerciseGroups.length === 0 ? (
|
||||
<p>None</p>
|
||||
) : (
|
||||
exerciseGroups.map((ex) => (
|
||||
<Row className="mb-3" key={ex.index}>
|
||||
<Col xs={5} className="mt-2">
|
||||
{ex.name}
|
||||
</Col>
|
||||
|
||||
<Col xs={1} className="mt-2">
|
||||
Reps
|
||||
</Col>
|
||||
|
||||
<Col xs={2}>
|
||||
<Form.Control id={`${ex.id}-reps`} defaultValue={ex.reps} />
|
||||
</Col>
|
||||
|
||||
<Col xs={1} className="mt-2">
|
||||
Sets
|
||||
</Col>
|
||||
|
||||
<Col xs={2}>
|
||||
<Form.Control id={`${ex.id}-sets`} defaultValue={ex.sets} />
|
||||
</Col>
|
||||
|
||||
<Col xs={1} className="mt-2">
|
||||
{/* TODO: Make this tooltip appear next to the image */}
|
||||
<OverlayTrigger
|
||||
overlay={<Tooltip>Delete {ex.name}</Tooltip>}
|
||||
>
|
||||
{({ ref }) => (
|
||||
<Image
|
||||
src="/images/delete.svg"
|
||||
alt={`Delete ${ex.name}`}
|
||||
height={20}
|
||||
width={20}
|
||||
onClick={() => deleteExercise(ex.index)}
|
||||
lazyRoot={ref}
|
||||
/>
|
||||
)}
|
||||
</OverlayTrigger>
|
||||
</Col>
|
||||
</Row>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</Form.Group>
|
||||
|
||||
<Button variant="primary" type="submit">
|
||||
Submit
|
||||
</Button>
|
||||
<div className={styles.formbuttons}>
|
||||
<Button variant="primary" onClick={handleModalOpen}>
|
||||
Add an exercise
|
||||
</Button>
|
||||
<div>
|
||||
<Link href="/workouts" passHref>
|
||||
<Button variant="secondary">Cancel</Button>
|
||||
</Link>{' '}
|
||||
<Button variant="primary" type="submit">
|
||||
Submit
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Form>
|
||||
|
||||
{displayAlert(isAlertActive)}
|
||||
|
||||
<Modal show={isModalOpen} onHide={handleModalClose} centered size="lg">
|
||||
<Modal.Header>
|
||||
<Modal.Title>
|
||||
<p>Add an exercise</p>
|
||||
</Modal.Title>
|
||||
</Modal.Header>
|
||||
|
||||
<Modal.Body>
|
||||
{exercises && (
|
||||
<List
|
||||
list={exercises}
|
||||
listType="radio"
|
||||
type="exercises"
|
||||
setSelected={setSelectedExercise}
|
||||
/>
|
||||
)}
|
||||
</Modal.Body>
|
||||
|
||||
<Modal.Footer>
|
||||
<Button variant="secondary" onClick={handleModalClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
|
||||
<Button variant="primary" onClick={handleModalClose}>
|
||||
Add
|
||||
</Button>
|
||||
</Modal.Footer>
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function EditWorkout() {
|
||||
export default function CreateWorkout() {
|
||||
return (
|
||||
<>
|
||||
<TopNavbar />
|
||||
|
|
|
@ -134,44 +134,35 @@ export default function WorkoutsPage() {
|
|||
)}
|
||||
|
||||
<Col>
|
||||
<div>
|
||||
<main className={styles.main}>
|
||||
<List
|
||||
list={workoutList}
|
||||
listType="radio"
|
||||
selected={selectedWorkout}
|
||||
setSelected={onClick}
|
||||
type="workouts"
|
||||
onDelete={onDelete}
|
||||
/>
|
||||
</main>
|
||||
</div>
|
||||
<List
|
||||
list={workoutList}
|
||||
listType="radio"
|
||||
selected={selectedWorkout}
|
||||
setSelected={onClick}
|
||||
type="workouts"
|
||||
onDelete={onDelete}
|
||||
allowEditing={authUser !== undefined}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
{selectedWorkout !== undefined && (
|
||||
<Modal
|
||||
show={isOpen}
|
||||
onHide={handleClose}
|
||||
centered
|
||||
scrollable
|
||||
size="lg"
|
||||
>
|
||||
<Modal.Header closeButton>
|
||||
<Modal.Title>{selectedWorkout.name}</Modal.Title>
|
||||
</Modal.Header>
|
||||
|
||||
{/* TODO: Mobile view for workouts. */}
|
||||
<Modal.Body>.</Modal.Body>
|
||||
|
||||
<Modal.Footer>
|
||||
<Button variant="primary" onClick={handleClose}>
|
||||
Close
|
||||
</Button>
|
||||
</Modal.Footer>
|
||||
</Modal>
|
||||
)}
|
||||
</Container>
|
||||
|
||||
{selectedWorkout !== undefined && (
|
||||
<Modal show={isOpen} onHide={handleClose} centered scrollable size="lg">
|
||||
<Modal.Header closeButton>
|
||||
<Modal.Title>{selectedWorkout.name}</Modal.Title>
|
||||
</Modal.Header>
|
||||
|
||||
{/* TODO: Mobile view for workouts. */}
|
||||
<Modal.Body>.</Modal.Body>
|
||||
|
||||
<Modal.Footer>
|
||||
<Button variant="primary" onClick={handleClose}>
|
||||
Close
|
||||
</Button>
|
||||
</Modal.Footer>
|
||||
</Modal>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" height="48" width="48"><path d="M13.05 42q-1.25 0-2.125-.875T10.05 39V10.5H8v-3h9.4V6h13.2v1.5H40v3h-2.05V39q0 1.2-.9 2.1-.9.9-2.1.9Zm21.9-31.5h-21.9V39h21.9Zm-16.6 24.2h3V14.75h-3Zm8.3 0h3V14.75h-3Zm-13.6-24.2V39Z"/></svg>
|
After Width: | Height: | Size: 263 B |
|
@ -7,6 +7,23 @@
|
|||
margin-bottom: 5vh;
|
||||
}
|
||||
|
||||
.formname {
|
||||
padding: 20px 0;
|
||||
}
|
||||
|
||||
.formexercises {
|
||||
border: 1px solid black;
|
||||
border-radius: 10px;
|
||||
padding: 2vh 2vh;
|
||||
margin-bottom: 1vh;
|
||||
}
|
||||
|
||||
.formbuttons {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.toast {
|
||||
position: fixed;
|
||||
top: 10vh;
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
/* Contains the image and the text */
|
||||
.element {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
width: 25vw + 120px + 60px;
|
||||
border: 1px solid black;
|
||||
border-radius: 10px;
|
||||
|
@ -14,7 +15,6 @@
|
|||
margin: 10px 0 0 5px;
|
||||
text-align: left;
|
||||
text-overflow: ellipsis;
|
||||
width: 20vw;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
@ -32,10 +32,14 @@
|
|||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.buttons {
|
||||
display: inline-flex;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.star {
|
||||
margin-top: 21px;
|
||||
margin-left: 4px;
|
||||
padding-inline: 10px;
|
||||
align-self: center;
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 576px) {
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
margin: 0 auto;
|
||||
align-content: center;
|
||||
}
|
||||
|
||||
.main {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
|
|
Loading…
Reference in New Issue