Split exercise forms, fix selecting elements
This commit is contained in:
parent
20c030d4a0
commit
0f6be7635c
|
@ -0,0 +1,14 @@
|
|||
// React
|
||||
import React from 'react';
|
||||
|
||||
// Bootstrap components
|
||||
import Alert from 'react-bootstrap/Alert';
|
||||
|
||||
export default function CustomAlert({ heading, body, variant, onClose }) {
|
||||
return (
|
||||
<Alert variant={variant} onClose={onClose} dismissible>
|
||||
<Alert.Heading>{heading}</Alert.Heading>
|
||||
<p>{body}</p>
|
||||
</Alert>
|
||||
);
|
||||
}
|
|
@ -5,7 +5,7 @@ import React from 'react';
|
|||
import Toast from 'react-bootstrap/Toast';
|
||||
|
||||
// Styles
|
||||
import styles from '../../styles/Crud.module.css';
|
||||
import styles from '../../styles/EditButton.module.css';
|
||||
|
||||
export default function CustomToast({ title, body, error, onClose }) {
|
||||
return (
|
|
@ -17,7 +17,7 @@ import { db } from '../../firebase-config';
|
|||
// Custom components
|
||||
import CustomToast from './CustomToast';
|
||||
|
||||
export default function CRUDButton({ type, create, id, name }) {
|
||||
export default function EditButton({ type, id, name, onDelete }) {
|
||||
/* Handles state for the delete toast */
|
||||
const [isToastActive, setToastActive] = useState({});
|
||||
const handleToastOpen = ({ title, body, error }) => {
|
||||
|
@ -35,7 +35,6 @@ export default function CRUDButton({ type, create, id, name }) {
|
|||
};
|
||||
|
||||
const handleDelete = () => {
|
||||
// TODO: Refresh exercise/workout list after deletion.
|
||||
if (type === 'exercise') {
|
||||
deleteDoc(doc(db, 'exercises', id))
|
||||
.then(() => {
|
||||
|
@ -65,44 +64,29 @@ export default function CRUDButton({ type, create, id, name }) {
|
|||
});
|
||||
});
|
||||
}
|
||||
|
||||
onDelete(id);
|
||||
handleModalClose();
|
||||
};
|
||||
|
||||
// Makes either a button, or a dropdown button
|
||||
function makeButton(urlBase, toCreate) {
|
||||
// Get URL base
|
||||
let url = '';
|
||||
if (urlBase === 'exercise') {
|
||||
url = '/exercises/edit';
|
||||
} else if (urlBase === 'workout') {
|
||||
url = '/workouts/edit';
|
||||
}
|
||||
|
||||
// Only regular button for creating
|
||||
if (toCreate !== undefined) {
|
||||
return (
|
||||
<Link href={{ pathname: url, query: 'type=create' }} passHref>
|
||||
<Button variant="primary">New {urlBase}</Button>
|
||||
const makeButton = (title) => (
|
||||
<DropdownButton title="...">
|
||||
{title === 'exercise' ? (
|
||||
<Link href={`/exercises/edit/${id}`} passHref>
|
||||
<Dropdown.Item>Edit {title}</Dropdown.Item>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
// Dropdown for editing and deleting
|
||||
return (
|
||||
<DropdownButton title="...">
|
||||
<Link href={{ pathname: url, query: `type=edit&id=${id}` }} passHref>
|
||||
<Dropdown.Item>Edit {urlBase}</Dropdown.Item>
|
||||
) : (
|
||||
<Link href={`/workouts/edit/${id}`} passHref>
|
||||
<Dropdown.Item>Edit {title}</Dropdown.Item>
|
||||
</Link>
|
||||
<Dropdown.Item onClick={handleModalOpen}>
|
||||
Delete {urlBase}
|
||||
</Dropdown.Item>
|
||||
</DropdownButton>
|
||||
);
|
||||
}
|
||||
)}
|
||||
|
||||
<Dropdown.Item onClick={handleModalOpen}>Delete {title}</Dropdown.Item>
|
||||
</DropdownButton>
|
||||
);
|
||||
|
||||
// Creates the modal only if there is an id.
|
||||
function makeModal(toShow) {
|
||||
const makeModal = (toShow) => {
|
||||
if (toShow !== undefined) {
|
||||
return (
|
||||
<Modal show={isModalOpen} onHide={handleModalClose} centered size="lg">
|
||||
|
@ -127,7 +111,7 @@ export default function CRUDButton({ type, create, id, name }) {
|
|||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
// Creates the toast for deleting
|
||||
function makeToast({ title, body, error }) {
|
||||
|
@ -146,7 +130,7 @@ export default function CRUDButton({ type, create, id, name }) {
|
|||
|
||||
return (
|
||||
<>
|
||||
{makeButton(type, create)}
|
||||
{makeButton(type)}
|
||||
{makeModal(id)}
|
||||
{makeToast(isToastActive)}
|
||||
</>
|
|
@ -9,7 +9,7 @@ import { collection, updateDoc, doc } from 'firebase/firestore';
|
|||
import { db } from '../../firebase-config';
|
||||
|
||||
// Custom components
|
||||
import CRUDButton from '../CRUDButton/CRUDButton';
|
||||
import EditButton from '../EditButton/EditButton';
|
||||
|
||||
// Styles
|
||||
import styles from '../../styles/Element.module.css';
|
||||
|
@ -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, onClick }) {
|
||||
export default function Element({ element, type, onDelete }) {
|
||||
/* Paths of the images of the favourite button */
|
||||
const star = '/images/star.png';
|
||||
const starFilled = '/images/starFilled.png';
|
||||
|
@ -123,21 +123,27 @@ export default function Element({ element, type, onClick }) {
|
|||
/* Crude check for checking if the element is an exercise or workout */
|
||||
if (element.instructions !== undefined) {
|
||||
return (
|
||||
<CRUDButton type="exercise" id={element.id} name={element.name} />
|
||||
<EditButton
|
||||
type="exercise"
|
||||
id={element.id}
|
||||
name={element.name}
|
||||
onDelete={onDelete}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return <CRUDButton type="workout" id={element.id} name={element.name} />;
|
||||
return (
|
||||
<EditButton
|
||||
type="workout"
|
||||
id={element.id}
|
||||
name={element.name}
|
||||
onDelete={onDelete}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
return (
|
||||
<div
|
||||
className={styles.element}
|
||||
onClick={onClick}
|
||||
onKeyPress={onClick}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
>
|
||||
<div className={styles.element}>
|
||||
<Image
|
||||
src={element.imageSource}
|
||||
alt={element.imageAlt}
|
||||
|
|
|
@ -17,6 +17,8 @@ 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" or "workouts"
|
||||
* @param {*} onDelete The callback function to handle an element being deleted from the list.
|
||||
* @returns
|
||||
*/
|
||||
export default function List({
|
||||
|
@ -25,7 +27,7 @@ export default function List({
|
|||
selected,
|
||||
setSelected,
|
||||
type,
|
||||
onClick,
|
||||
onDelete,
|
||||
}) {
|
||||
// A function to handle when a new element is selected
|
||||
const handleChange = (e) => {
|
||||
|
@ -62,12 +64,12 @@ export default function List({
|
|||
id={`${listType}-${element.id}`}
|
||||
variant="light"
|
||||
name={listType}
|
||||
value={element.id}
|
||||
value={element}
|
||||
>
|
||||
{selected === element.name ? (
|
||||
<SelectedElement element={element} type={type} />
|
||||
) : (
|
||||
<Element element={element} type={type} onClick={onClick} />
|
||||
<Element element={element} type={type} onDelete={onDelete} />
|
||||
)}
|
||||
</ToggleButton>
|
||||
))}
|
||||
|
|
|
@ -53,23 +53,11 @@ export default function TopNavbar() {
|
|||
<Nav>
|
||||
{authUser &&
|
||||
(router.pathname.includes('exercises') ? (
|
||||
<Link
|
||||
href={{
|
||||
pathname: '/exercises/edit',
|
||||
query: 'type=create',
|
||||
}}
|
||||
passHref
|
||||
>
|
||||
<Link href="/exercises/create" passHref>
|
||||
<Nav.Link className={styles.item}>New exercise</Nav.Link>
|
||||
</Link>
|
||||
) : (
|
||||
<Link
|
||||
href={{
|
||||
pathname: '/workouts/edit',
|
||||
query: 'type=create',
|
||||
}}
|
||||
passHref
|
||||
>
|
||||
<Link href="/workouts/create" passHref>
|
||||
<Nav.Link className={styles.item}>New workout</Nav.Link>
|
||||
</Link>
|
||||
))}
|
||||
|
|
|
@ -12,6 +12,16 @@ const nextConfig = {
|
|||
destination: '/exercises',
|
||||
permanent: true,
|
||||
},
|
||||
{
|
||||
source: '/exercises/edit',
|
||||
destination: '/exercises',
|
||||
permanent: true,
|
||||
},
|
||||
{
|
||||
source: '/workouts/edit',
|
||||
destination: '/workouts',
|
||||
permanent: true,
|
||||
},
|
||||
]
|
||||
},
|
||||
};
|
||||
|
|
|
@ -0,0 +1,254 @@
|
|||
// React
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
|
||||
// Next components
|
||||
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 Row from 'react-bootstrap/Row';
|
||||
|
||||
// Firebase
|
||||
import { addDoc, collection } from 'firebase/firestore';
|
||||
import { db } from '../../../firebase-config';
|
||||
|
||||
// Custom components
|
||||
import CustomAlert from '../../../components/EditButton/CustomAlert';
|
||||
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 workouts collection
|
||||
const exercisesCollectionRef = collection(db, 'exercises');
|
||||
|
||||
// A form used for both creating and editing exercises
|
||||
function ExerciseForm() {
|
||||
/* Handles the state of the checkboxes */
|
||||
const [checkboxes, setCheckboxes] = useState([]);
|
||||
|
||||
/* 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 manage the list of chosen muscle groups in the form */
|
||||
const chosenMuscleGroups = useRef([]);
|
||||
|
||||
/* Used to prevent continuously creating the checkboxes */
|
||||
const isFirstLoad = useRef(false);
|
||||
|
||||
/* Use Router for automatic redirect after successful form submission */
|
||||
const router = useRouter();
|
||||
|
||||
useEffect(() => {
|
||||
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 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="exerciseMuscleGroups"
|
||||
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;
|
||||
};
|
||||
|
||||
if (!isFirstLoad.current) {
|
||||
setCheckboxes(makeCheckboxes);
|
||||
isFirstLoad.current = true;
|
||||
}
|
||||
}, [isFirstLoad]);
|
||||
|
||||
/* Handles the submission of forms. */
|
||||
const handleSubmit = async (event) => {
|
||||
/* Prevent automatic submission and refreshing of the page. */
|
||||
event.preventDefault();
|
||||
|
||||
/* TODO: Implement image uploading */
|
||||
const data = {
|
||||
name: event.target.exerciseName.value,
|
||||
videoURL: event.target.exerciseURL.value,
|
||||
instructions: event.target.exerciseInstructions.value,
|
||||
equipment: event.target.exerciseEquipment.value,
|
||||
imageSource: '/images/hammer-curls.png',
|
||||
imageAlt: `Picture of ${event.target.exerciseName.value}`,
|
||||
muscleGroups: chosenMuscleGroups.current,
|
||||
};
|
||||
|
||||
/* Send the form data to the API and get a response */
|
||||
const response = await fetch('/api/exercise', {
|
||||
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();
|
||||
addDoc(exercisesCollectionRef, result.data)
|
||||
.then(() => {
|
||||
handleAlertOpen({
|
||||
heading: 'Success!',
|
||||
body: `${result.data.name} was added to the exercise list. Redirecting...`,
|
||||
variant: 'success',
|
||||
});
|
||||
setTimeout(() => {
|
||||
router.push('/exercises');
|
||||
}, 3000);
|
||||
})
|
||||
.catch((error) => {
|
||||
handleAlertOpen({
|
||||
heading: 'Error',
|
||||
body: error,
|
||||
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;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={styles.form}>
|
||||
<h2>Creating new exercise</h2>
|
||||
|
||||
<Form onSubmit={handleSubmit} action="/api/exercise" method="post">
|
||||
<Form.Group>
|
||||
<Form.Label>Exercise name</Form.Label>
|
||||
<Form.Control
|
||||
id="exerciseName"
|
||||
type="text"
|
||||
placeholder="Enter exercise name"
|
||||
/>
|
||||
</Form.Group>
|
||||
|
||||
<Form.Group>
|
||||
<Form.Label>Select targeted areas</Form.Label>
|
||||
<Container fluid>
|
||||
<Row>{checkboxes}</Row>
|
||||
</Container>
|
||||
</Form.Group>
|
||||
|
||||
<Form.Group>
|
||||
<Form.Label>Enter video url to display</Form.Label>
|
||||
<Form.Control
|
||||
id="exerciseURL"
|
||||
type="url"
|
||||
placeholder="Enter video URL"
|
||||
/>
|
||||
</Form.Group>
|
||||
|
||||
{/* Not going to work just yet */}
|
||||
{/* <Form.Group controlId="formThumbnail">
|
||||
<Form.Label>Select a thumbnail</Form.Label>
|
||||
<Form.Control type="file" />
|
||||
</Form.Group>
|
||||
|
||||
<Form.Group controlId="formImageAlt">
|
||||
<Form.Label>Enter text to show if image doesn't load</Form.Label>
|
||||
{isEditingForm ? (
|
||||
<Form.Control type="imageAlt" defaultValue={exercise.imageAlt} />
|
||||
) : (
|
||||
<Form.Control type="imageAlt" placeholder="Enter image alt" />
|
||||
)}
|
||||
</Form.Group> */}
|
||||
|
||||
<Form.Group>
|
||||
<Form.Label>Equipment needed</Form.Label>
|
||||
<Form.Control
|
||||
id="exerciseEquipment"
|
||||
type="text"
|
||||
placeholder="Enter required equipment (leave blank for nothing)."
|
||||
/>
|
||||
</Form.Group>
|
||||
|
||||
<Form.Group>
|
||||
<Form.Label>Exercise instructions</Form.Label>
|
||||
<Form.Control
|
||||
type="text"
|
||||
id="exerciseInstructions"
|
||||
as="textarea"
|
||||
rows={5}
|
||||
placeholder="Enter instructions to complete the exercise."
|
||||
/>
|
||||
</Form.Group>
|
||||
|
||||
<Button variant="primary" type="submit">
|
||||
Submit
|
||||
</Button>
|
||||
</Form>
|
||||
|
||||
{displayAlert(isAlertActive)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function CreateExercise() {
|
||||
return (
|
||||
<>
|
||||
<TopNavbar />
|
||||
{/* TODO: Preview of changes on side? */}
|
||||
<div className={styles.main}>
|
||||
<ExerciseForm />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -5,7 +5,6 @@ import React, { useEffect, useRef, useState } from 'react';
|
|||
import { useRouter } from 'next/router';
|
||||
|
||||
// Bootstrap components
|
||||
import Alert from 'react-bootstrap/Alert';
|
||||
import Button from 'react-bootstrap/Button';
|
||||
import Container from 'react-bootstrap/Container';
|
||||
import Col from 'react-bootstrap/Col';
|
||||
|
@ -13,24 +12,24 @@ import Form from 'react-bootstrap/Form';
|
|||
import Row from 'react-bootstrap/Row';
|
||||
|
||||
// Firebase
|
||||
import { getDoc, collection, addDoc, updateDoc, doc } from 'firebase/firestore';
|
||||
import { getDoc, updateDoc, doc } from 'firebase/firestore';
|
||||
import { db } from '../../../firebase-config';
|
||||
|
||||
// Custom components
|
||||
import CustomAlert from '../../../components/EditButton/CustomAlert';
|
||||
import TopNavbar from '../../../components/Navbar/Navbar';
|
||||
|
||||
// Styles
|
||||
import styles from '../../../styles/Crud.module.css';
|
||||
import styles from '../../../styles/EditButton.module.css';
|
||||
|
||||
// Get muscle list
|
||||
import muscles from '../../../public/muscles.json' assert { type: 'json' };
|
||||
|
||||
// Get reference to workouts collection
|
||||
const exercisesCollectionRef = collection(db, 'exercises');
|
||||
|
||||
// A form used for both creating and editing exercises
|
||||
function ExerciseForm({ id }) {
|
||||
const isEditingForm = id !== undefined;
|
||||
function ExerciseForm() {
|
||||
/* Get exercise ID from query parameters */
|
||||
const router = useRouter();
|
||||
const { id } = router.query;
|
||||
|
||||
/* Handles state for the exercise */
|
||||
const [exercise, setExercise] = useState({});
|
||||
|
@ -55,24 +54,15 @@ function ExerciseForm({ id }) {
|
|||
const chosenMuscleGroups = useRef([]);
|
||||
|
||||
/* Used to ensure that the exercises collection is queried only once */
|
||||
const [isExercisesReceived, setExercisesReceived] = useState(false);
|
||||
|
||||
/* Use Router for automatic redirect after successful form submission */
|
||||
const router = useRouter();
|
||||
const isFirstLoad = useRef(false);
|
||||
|
||||
useEffect(() => {
|
||||
const getExercise = async () => {
|
||||
if (isEditingForm) {
|
||||
const exerciseDoc = await getDoc(doc(db, 'exercises', id));
|
||||
chosenMuscleGroups.current = exerciseDoc.data().muscleGroups;
|
||||
setExercise(exerciseDoc.data());
|
||||
}
|
||||
const exerciseDoc = await getDoc(doc(db, 'exercises', id));
|
||||
chosenMuscleGroups.current = exerciseDoc.data().muscleGroups;
|
||||
setExercise(exerciseDoc.data());
|
||||
};
|
||||
|
||||
if (!isExercisesReceived) {
|
||||
getExercise();
|
||||
}
|
||||
|
||||
const updateChosenMuscles = (ex) => {
|
||||
if (chosenMuscleGroups.current.includes(ex.target.value)) {
|
||||
const filtered = chosenMuscleGroups.current.filter(
|
||||
|
@ -86,7 +76,7 @@ function ExerciseForm({ id }) {
|
|||
|
||||
const makeCheckboxes = () => {
|
||||
const checkboxColumns = [];
|
||||
// Create checkboxes for all muscles, and pre-check boxes if editing
|
||||
// Create checkboxes for all muscles, and pre-check boxes
|
||||
const preChecked =
|
||||
exercise.muscleGroups !== undefined ? exercise.muscleGroups : [];
|
||||
for (let i = 0; i < muscles.length; i += 1) {
|
||||
|
@ -94,9 +84,9 @@ function ExerciseForm({ id }) {
|
|||
const boxes = [];
|
||||
for (let j = 0; j < musclesList.length; j += 1) {
|
||||
const { mId, name } = musclesList[j];
|
||||
boxes.push(
|
||||
<div className="mb-3" key={mId}>
|
||||
{preChecked.includes(name) ? (
|
||||
if (preChecked.includes(name)) {
|
||||
boxes.push(
|
||||
<div className="mb-3" key={mId}>
|
||||
<Form.Check
|
||||
type="checkbox"
|
||||
id="exerciseMuscleGroups"
|
||||
|
@ -106,7 +96,11 @@ function ExerciseForm({ id }) {
|
|||
defaultChecked
|
||||
onChange={updateChosenMuscles}
|
||||
/>
|
||||
) : (
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
boxes.push(
|
||||
<div className="mb-3" key={mId}>
|
||||
<Form.Check
|
||||
type="checkbox"
|
||||
id="exerciseMuscleGroups"
|
||||
|
@ -115,9 +109,9 @@ function ExerciseForm({ id }) {
|
|||
key={name}
|
||||
onChange={updateChosenMuscles}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
/* TODO: This is still giving unique key errors, not sure why. */
|
||||
checkboxColumns.push(
|
||||
|
@ -130,21 +124,19 @@ function ExerciseForm({ id }) {
|
|||
return checkboxColumns;
|
||||
};
|
||||
|
||||
if (isExercisesReceived) {
|
||||
setCheckboxes(makeCheckboxes);
|
||||
// TODO: Make the checkboxes pre-checked
|
||||
if (!isFirstLoad.current) {
|
||||
getExercise().then(setCheckboxes(makeCheckboxes()));
|
||||
isFirstLoad.current = true;
|
||||
}
|
||||
|
||||
return () => {
|
||||
setExercisesReceived(true);
|
||||
};
|
||||
}, [id, exercise.muscleGroups, router, isEditingForm, isExercisesReceived]);
|
||||
}, [exercise.muscleGroups, id]);
|
||||
|
||||
/* Handles the submission of forms. */
|
||||
const handleSubmit = async (event) => {
|
||||
/* Prevent automatic submission and refreshing of the page. */
|
||||
event.preventDefault();
|
||||
|
||||
/* TODO: Implement muscleGroups and image uploading */
|
||||
/* TODO: Implement image uploading */
|
||||
const data = {
|
||||
name: event.target.exerciseName.value,
|
||||
videoURL: event.target.exerciseURL.value,
|
||||
|
@ -164,56 +156,37 @@ function ExerciseForm({ id }) {
|
|||
method: 'POST',
|
||||
});
|
||||
|
||||
/* Get the response, update/add the document, create an alert, then redirect. */
|
||||
/* Get the response, update the document, create an alert, then redirect. */
|
||||
const result = await response.json();
|
||||
if (isEditingForm) {
|
||||
updateDoc(doc(db, 'exercises', id), result.data)
|
||||
.then(() => {
|
||||
handleAlertOpen({
|
||||
heading: 'Success!',
|
||||
body: `${result.data.name} was updated in the exercise list. Redirecting...`,
|
||||
variant: 'success',
|
||||
});
|
||||
setTimeout(() => {
|
||||
router.push('/exercises');
|
||||
}, 3000);
|
||||
})
|
||||
.catch((error) => {
|
||||
handleAlertOpen({
|
||||
heading: 'Error',
|
||||
body: error,
|
||||
variant: 'danger',
|
||||
});
|
||||
updateDoc(doc(db, 'exercises', id), result.data)
|
||||
.then(() => {
|
||||
handleAlertOpen({
|
||||
heading: 'Success!',
|
||||
body: `${result.data.name} was updated in the exercise list. Redirecting...`,
|
||||
variant: 'success',
|
||||
});
|
||||
} else {
|
||||
addDoc(exercisesCollectionRef, result.data)
|
||||
.then(() => {
|
||||
handleAlertOpen({
|
||||
heading: 'Success!',
|
||||
body: `${result.data.name} was added to the exercise list. Redirecting...`,
|
||||
variant: 'success',
|
||||
});
|
||||
setTimeout(() => {
|
||||
router.push('/exercises');
|
||||
}, 3000);
|
||||
})
|
||||
.catch((error) => {
|
||||
handleAlertOpen({
|
||||
heading: 'Error',
|
||||
body: error,
|
||||
variant: 'danger',
|
||||
});
|
||||
setTimeout(() => {
|
||||
router.push('/exercises');
|
||||
}, 3000);
|
||||
})
|
||||
.catch((error) => {
|
||||
handleAlertOpen({
|
||||
heading: 'Error',
|
||||
body: error,
|
||||
variant: 'danger',
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const displayAlert = ({ heading, body, variant }) => {
|
||||
if (heading !== undefined) {
|
||||
if (heading && body && variant) {
|
||||
return (
|
||||
<Alert variant={variant} onClose={handleAlertClose} dismissible>
|
||||
<Alert.Heading>{heading}</Alert.Heading>
|
||||
<p>{body}</p>
|
||||
</Alert>
|
||||
<CustomAlert
|
||||
heading={heading}
|
||||
body={body}
|
||||
variant={variant}
|
||||
onClose={handleAlertClose}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
|
@ -221,28 +194,16 @@ function ExerciseForm({ id }) {
|
|||
|
||||
return (
|
||||
<div className={styles.form}>
|
||||
{displayAlert(isAlertActive)}
|
||||
|
||||
<h2>
|
||||
{isEditingForm ? `Editing '${exercise.name}'` : 'Creating new exercise'}
|
||||
</h2>
|
||||
<h2>Editing {exercise.name}</h2>
|
||||
|
||||
<Form onSubmit={handleSubmit} action="/api/exercise" method="post">
|
||||
<Form.Group>
|
||||
<Form.Label>Exercise name</Form.Label>
|
||||
{isEditingForm ? (
|
||||
<Form.Control
|
||||
id="exerciseName"
|
||||
type="text"
|
||||
defaultValue={exercise.name}
|
||||
/>
|
||||
) : (
|
||||
<Form.Control
|
||||
id="exerciseName"
|
||||
type="text"
|
||||
placeholder="Enter exercise name"
|
||||
/>
|
||||
)}
|
||||
<Form.Control
|
||||
id="exerciseName"
|
||||
type="text"
|
||||
defaultValue={exercise.name}
|
||||
/>
|
||||
</Form.Group>
|
||||
|
||||
<Form.Group>
|
||||
|
@ -254,19 +215,11 @@ function ExerciseForm({ id }) {
|
|||
|
||||
<Form.Group>
|
||||
<Form.Label>Enter video url to display</Form.Label>
|
||||
{isEditingForm ? (
|
||||
<Form.Control
|
||||
id="exerciseURL"
|
||||
type="url"
|
||||
defaultValue={exercise.videoURL}
|
||||
/>
|
||||
) : (
|
||||
<Form.Control
|
||||
id="exerciseURL"
|
||||
type="url"
|
||||
placeholder="Enter video URL"
|
||||
/>
|
||||
)}
|
||||
<Form.Control
|
||||
id="exerciseURL"
|
||||
type="url"
|
||||
defaultValue={exercise.videoURL}
|
||||
/>
|
||||
</Form.Group>
|
||||
|
||||
{/* Not going to work just yet */}
|
||||
|
@ -286,78 +239,42 @@ function ExerciseForm({ id }) {
|
|||
|
||||
<Form.Group>
|
||||
<Form.Label>Equipment needed</Form.Label>
|
||||
{isEditingForm ? (
|
||||
<Form.Control
|
||||
id="exerciseEquipment"
|
||||
type="text"
|
||||
defaultValue={exercise.equipment}
|
||||
/>
|
||||
) : (
|
||||
<Form.Control
|
||||
id="exerciseEquipment"
|
||||
type="text"
|
||||
placeholder="Enter required equipment"
|
||||
/>
|
||||
)}
|
||||
<Form.Control
|
||||
id="exerciseEquipment"
|
||||
type="text"
|
||||
defaultValue={exercise.equipment}
|
||||
/>
|
||||
</Form.Group>
|
||||
|
||||
<Form.Group>
|
||||
<Form.Label>Exercise instructions</Form.Label>
|
||||
{isEditingForm ? (
|
||||
<Form.Control
|
||||
type="text"
|
||||
id="exerciseInstructions"
|
||||
as="textarea"
|
||||
rows={5}
|
||||
defaultValue={exercise.instructions}
|
||||
/>
|
||||
) : (
|
||||
<Form.Control
|
||||
type="text"
|
||||
id="exerciseInstructions"
|
||||
as="textarea"
|
||||
rows={5}
|
||||
placeholder="Enter instructions to complete the exercise. Split into steps by entering a newline."
|
||||
/>
|
||||
)}
|
||||
<Form.Control
|
||||
type="text"
|
||||
id="exerciseInstructions"
|
||||
as="textarea"
|
||||
rows={5}
|
||||
defaultValue={exercise.instructions}
|
||||
/>
|
||||
</Form.Group>
|
||||
|
||||
<Button variant="primary" type="submit">
|
||||
Submit
|
||||
</Button>
|
||||
</Form>
|
||||
|
||||
{displayAlert(isAlertActive)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// TODO: Ensure this page can only be accessed if signed in as admin
|
||||
export default function EditExercise() {
|
||||
// Get operation type and exercise ID from query parameters
|
||||
const router = useRouter();
|
||||
const { id } = router.query;
|
||||
|
||||
return (
|
||||
<>
|
||||
<TopNavbar />
|
||||
{/* TODO: Preview of changes on side. */}
|
||||
<div className={styles.main}>
|
||||
{id !== undefined ? <ExerciseForm id={id} /> : <ExerciseForm />}
|
||||
<ExerciseForm />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export async function getServerSideProps({ query }) {
|
||||
if (query.type !== 'create' && query.type !== 'edit') {
|
||||
return {
|
||||
redirect: {
|
||||
permanent: false,
|
||||
destination: '/exercises',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
props: {},
|
||||
};
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
// React
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import React, { useEffect, useState, useRef } from 'react';
|
||||
|
||||
// Bootstrap components
|
||||
import Button from 'react-bootstrap/Button';
|
||||
|
@ -39,7 +39,7 @@ export default function ExercisesPage() {
|
|||
const [exercises, setExercises] = useState([]);
|
||||
|
||||
/* Used to ensure the database is only accessed once */
|
||||
const [isExercisesLoaded, setExercisesLoaded] = useState(false);
|
||||
const isExercisesLoaded = useRef(false);
|
||||
|
||||
/* Only render Card if innerWidth > 576px (small breakpoint) */
|
||||
const [toRenderCard, setRenderCard] = useState(true);
|
||||
|
@ -50,13 +50,13 @@ export default function ExercisesPage() {
|
|||
setExercises(data.docs.map((doc) => ({ ...doc.data(), id: doc.id })));
|
||||
};
|
||||
|
||||
if (!isExercisesLoaded) {
|
||||
if (!isExercisesLoaded.current) {
|
||||
getExercises();
|
||||
setExercisesLoaded(true);
|
||||
isExercisesLoaded.current = true;
|
||||
}
|
||||
|
||||
/* Get the user's favourites to bump them to the top of the exercise list */
|
||||
if (isExercisesLoaded) {
|
||||
if (isExercisesLoaded.current) {
|
||||
if (authUser) {
|
||||
const favs = exercises.filter((doc) =>
|
||||
authUser.favouriteExercises.includes(doc.id)
|
||||
|
@ -77,7 +77,7 @@ export default function ExercisesPage() {
|
|||
if (window.innerWidth < 576) {
|
||||
setRenderCard(false);
|
||||
}
|
||||
}, [authUser, selectedExercise, exerciseList, isExercisesLoaded, exercises]);
|
||||
}, [authUser, exercises]);
|
||||
|
||||
const [isOpen, setOpen] = useState(false);
|
||||
const handleOpen = () => setOpen(true);
|
||||
|
@ -88,14 +88,18 @@ export default function ExercisesPage() {
|
|||
* If the window width is small, the modal is opened when the elements are
|
||||
* clicked. Otherwise, the card is updated.
|
||||
*/
|
||||
const onClick = () => {
|
||||
const onClick = (ex) => {
|
||||
setSelectedExercise(ex);
|
||||
if (!toRenderCard) {
|
||||
handleOpen();
|
||||
} else {
|
||||
setSelectedExercise(selectedExercise.id);
|
||||
}
|
||||
};
|
||||
|
||||
/* When an exercise is deleted, remove it from the list. */
|
||||
const onDelete = (id) => {
|
||||
setExerciseList(exercises.filter((doc) => doc.id !== id));
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<TopNavbar />
|
||||
|
@ -128,9 +132,9 @@ export default function ExercisesPage() {
|
|||
list={exerciseList}
|
||||
listType="radio"
|
||||
selected={selectedExercise}
|
||||
setSelected={setSelectedExercise}
|
||||
setSelected={onClick}
|
||||
type="exercises"
|
||||
onClick={onClick}
|
||||
onDelete={onDelete}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
|
|
|
@ -78,7 +78,7 @@ export default function WorkoutsPage() {
|
|||
if (window.innerWidth < 576) {
|
||||
setRenderCard(false);
|
||||
}
|
||||
}, [authUser, isWorkoutsLoaded, selected, workoutList, workouts]);
|
||||
}, [authUser, isWorkoutsLoaded, workouts]);
|
||||
|
||||
const [isOpen, setOpen] = useState(false);
|
||||
const handleOpen = () => setOpen(true);
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
|
||||
.form {
|
||||
padding: 0 10vw;
|
||||
margin-bottom: 5vh;
|
||||
}
|
||||
|
||||
.toast {
|
Loading…
Reference in New Issue