merge main into feature-exercise-crud
This commit is contained in:
commit
2bb053ee97
|
@ -4,36 +4,10 @@ import React from 'react';
|
|||
// Styles
|
||||
import styles from '../styles/Instructions.module.css';
|
||||
|
||||
export default function Instructions() {
|
||||
export default function Instructions({ text }) {
|
||||
return (
|
||||
<div className={styles.instructions}>
|
||||
<p>
|
||||
<span className={styles.line}>
|
||||
Step 1: Stand up straight with your torso upright. Hold a dumbbell in
|
||||
each hand at arms-length. Your elbows should be close to your torso.
|
||||
</span>
|
||||
|
||||
<span className={styles.line}>
|
||||
Step 2: The palms of your hands should be facing your torso. This is
|
||||
the starting position for the exercise.
|
||||
</span>
|
||||
|
||||
<span className={styles.line}>
|
||||
Step 3: Curl the weight forward while contracting your biceps. Your
|
||||
upper arm should remain stationary. Continue to lift the weight until
|
||||
your biceps are fully contracted and the dumbbell is at shoulder
|
||||
level. Hold the contraction for a moment as you squeeze your biceps.
|
||||
</span>
|
||||
|
||||
<span className={styles.line}>
|
||||
Step 4: Inhale and slowly start to bring the dumbbells back to the
|
||||
starting position.
|
||||
</span>
|
||||
|
||||
<span className={styles.line}>
|
||||
Step 5: Repeat for the desired number of reps.
|
||||
</span>
|
||||
</p>
|
||||
<span className={styles.line}>{text}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,30 +1,115 @@
|
|||
// React
|
||||
import React, { useState } from 'react';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
|
||||
// Next components
|
||||
import Image from 'next/image';
|
||||
|
||||
// Firebase
|
||||
import { collection, updateDoc, doc } from 'firebase/firestore';
|
||||
import { db } from '../../firebase-config';
|
||||
|
||||
// Custom components
|
||||
import CRUDButton from '../CRUDButton/CRUDButton';
|
||||
|
||||
// Styles
|
||||
import styles from '../../styles/List.module.css';
|
||||
|
||||
// Star images
|
||||
// Authentication
|
||||
import { useAuth } from '../../context/authUserContext';
|
||||
|
||||
const star = '/images/star.png';
|
||||
const starFilled = '/images/starFilled.png';
|
||||
|
||||
export default function Element({ element }) {
|
||||
const usersCollectionRef = collection(db, 'users');
|
||||
|
||||
export default function Element({ element, type }) {
|
||||
// State of the image that is displayed as the favourite button
|
||||
const [imgPath, setImgPath] = useState(star);
|
||||
|
||||
const { authUser } = useAuth();
|
||||
|
||||
useEffect(() => {
|
||||
const isChecked = () => {
|
||||
if (authUser) {
|
||||
if (type === 'workouts') {
|
||||
if (authUser.favouriteWorkouts.includes(element.id)) {
|
||||
setImgPath(starFilled);
|
||||
} else {
|
||||
setImgPath(star);
|
||||
}
|
||||
}
|
||||
if (type === 'exercises') {
|
||||
if (authUser.favouriteExercises.includes(element.id)) {
|
||||
setImgPath(starFilled);
|
||||
} else {
|
||||
setImgPath(star);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
setImgPath(star);
|
||||
}
|
||||
};
|
||||
isChecked();
|
||||
}, [authUser, element.id, type]);
|
||||
|
||||
const updateFavWorkouts = async (newFavs) => {
|
||||
authUser.favouriteWorkouts = newFavs;
|
||||
const docRef = doc(usersCollectionRef, authUser.uid);
|
||||
await updateDoc(docRef, {
|
||||
favouriteWorkouts: newFavs,
|
||||
});
|
||||
};
|
||||
|
||||
const updateFavExercises = async (newFavs) => {
|
||||
authUser.favouriteExercises = newFavs;
|
||||
const docRef = doc(usersCollectionRef, authUser.uid);
|
||||
await updateDoc(docRef, {
|
||||
favouriteExercises: newFavs,
|
||||
});
|
||||
};
|
||||
|
||||
const removeFavWorkout = (elem) => {
|
||||
const newFavs = authUser.favouriteWorkouts.filter((val) => val !== elem);
|
||||
updateFavWorkouts(newFavs);
|
||||
};
|
||||
|
||||
const addToFavWorkouts = async (elem) => {
|
||||
authUser.favouriteWorkouts.push(elem);
|
||||
updateFavWorkouts(authUser.favouriteWorkouts);
|
||||
};
|
||||
|
||||
const removeFavExercise = async (elem) => {
|
||||
const newFavs = authUser.favouriteExercises.filter((val) => val !== elem);
|
||||
updateFavExercises(newFavs);
|
||||
};
|
||||
|
||||
const addToFavExercises = async (elem) => {
|
||||
authUser.favouriteExercises.push(elem);
|
||||
updateFavExercises(authUser.favouriteExercises);
|
||||
};
|
||||
|
||||
// Event handler if the favourite button is clicked on
|
||||
const toggleStar = (e) => {
|
||||
e.preventDefault();
|
||||
if (imgPath === star) {
|
||||
setImgPath(starFilled);
|
||||
} else {
|
||||
setImgPath(star);
|
||||
if (authUser) {
|
||||
if (type === 'workouts') {
|
||||
if (authUser.favouriteWorkouts.includes(element.id)) {
|
||||
removeFavWorkout(element.id);
|
||||
setImgPath(star);
|
||||
} else {
|
||||
addToFavWorkouts(element.id);
|
||||
setImgPath(starFilled);
|
||||
}
|
||||
}
|
||||
if (type === 'exercises') {
|
||||
if (authUser.favouriteExercises.includes(element.id)) {
|
||||
removeFavExercise(element.id);
|
||||
setImgPath(star);
|
||||
} else {
|
||||
addToFavExercises(element.id);
|
||||
setImgPath(starFilled);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -3,16 +3,18 @@ import React from 'react';
|
|||
import { fireEvent, render, screen } from '@testing-library/react';
|
||||
|
||||
// Custom components
|
||||
import Element from './Element';
|
||||
import Workout from '../../public/classes/Workout';
|
||||
import ElementTest from '../Test/ElementTest';
|
||||
|
||||
// Test Data
|
||||
import { workouts } from '../../testData/testData';
|
||||
|
||||
const toggleImgPath = (src) =>
|
||||
src.includes('star.png') ? '/images/starFilled.png' : '/images/star.png';
|
||||
|
||||
describe('The Favourite button', () => {
|
||||
it('Toggles on and off when clicked', () => {
|
||||
const element = new Workout('Pull Workout', ['Back', 'Biceps', 'Abs']);
|
||||
render(<Element element={element} />);
|
||||
const element = workouts[0];
|
||||
render(<ElementTest element={element} type="workouts" />);
|
||||
|
||||
const favBtn = screen.getByRole('button');
|
||||
let expectedImgPath = toggleImgPath(favBtn.getAttribute('src'));
|
||||
|
|
|
@ -9,6 +9,7 @@ import ToggleButtonGroup from 'react-bootstrap/ToggleButtonGroup';
|
|||
import SearchFilterBar from './SearchFilterBar';
|
||||
import Element from './Element';
|
||||
import styles from '../../styles/List.module.css';
|
||||
import SelectedElement from './SelectedElement';
|
||||
|
||||
/**
|
||||
*
|
||||
|
@ -18,7 +19,7 @@ import styles from '../../styles/List.module.css';
|
|||
* @param {*} setSelected The function that sets the state of selected
|
||||
* @returns
|
||||
*/
|
||||
export default function List({ list, listType, selected, setSelected }) {
|
||||
export default function List({ list, listType, selected, setSelected, type }) {
|
||||
// A function to handle when a new element is selected
|
||||
const handleChange = (e) => {
|
||||
setSelected(e);
|
||||
|
@ -53,13 +54,17 @@ export default function List({ list, listType, selected, setSelected }) {
|
|||
>
|
||||
{filteredList.map((element) => (
|
||||
<ToggleButton
|
||||
key={element.name}
|
||||
id={`${listType}-${element.name}`}
|
||||
key={element.id}
|
||||
id={`${listType}-${element.id}`}
|
||||
variant="light"
|
||||
name={listType}
|
||||
value={element.name}
|
||||
value={element.id}
|
||||
>
|
||||
<Element element={element} />
|
||||
{selected === element.name ? (
|
||||
<SelectedElement element={element} type={type} />
|
||||
) : (
|
||||
<Element element={element} type={type} />
|
||||
)}
|
||||
</ToggleButton>
|
||||
))}
|
||||
</ToggleButtonGroup>
|
||||
|
|
|
@ -5,6 +5,8 @@ import React from 'react';
|
|||
import Form from 'react-bootstrap/Form';
|
||||
import InputGroup from 'react-bootstrap/InputGroup';
|
||||
|
||||
import styles from '../../styles/Search.module.css';
|
||||
|
||||
export default function SearchFilterBar({ setSearchInput }) {
|
||||
// Function to handle when the search input changes
|
||||
const handleSearchInput = (e) => {
|
||||
|
@ -20,8 +22,11 @@ export default function SearchFilterBar({ setSearchInput }) {
|
|||
onChange={handleSearchInput}
|
||||
/>
|
||||
|
||||
<Form.Select aria-label="Default select example">
|
||||
<option>Filter Muscle Group</option>
|
||||
<Form.Select
|
||||
aria-label="Default select example"
|
||||
className={styles.filter}
|
||||
>
|
||||
<option>Filter</option>
|
||||
<option value="1">Chest</option>
|
||||
<option value="2">Back</option>
|
||||
<option value="3">Hamstrings</option>
|
||||
|
|
|
@ -1,17 +1,10 @@
|
|||
// React
|
||||
import React from 'react';
|
||||
import { fireEvent, render } from '@testing-library/react';
|
||||
import List from './List';
|
||||
import Workout from '../../public/classes/Workout';
|
||||
import ListTest from '../Test/ListTest';
|
||||
|
||||
// Dummy data to render in List component
|
||||
const workoutList = [];
|
||||
workoutList.push(
|
||||
new Workout('Push Workout', ['Chest', 'Shoulder', 'Triceps']),
|
||||
new Workout('Pull Workout', ['Back', 'Biceps', 'Abs']),
|
||||
new Workout('Legs Workout', ['Quadriceps', 'Hamstrings', 'Calves']),
|
||||
new Workout('Upper Workout', ['Chest', 'Back', 'Shoulder', 'Triceps'])
|
||||
);
|
||||
import { workouts } from '../../testData/testData';
|
||||
|
||||
describe('The Search Bar', () => {
|
||||
it('filters the correct items on lowercase input "pu"', () => {
|
||||
|
@ -20,8 +13,8 @@ describe('The Search Bar', () => {
|
|||
const setSelected = jest.fn();
|
||||
|
||||
const list = render(
|
||||
<List
|
||||
list={workoutList}
|
||||
<ListTest
|
||||
list={workouts}
|
||||
selected={selected}
|
||||
setSelected={setSelected}
|
||||
type="radio"
|
||||
|
@ -48,8 +41,8 @@ describe('The Search Bar', () => {
|
|||
const setSelected = jest.fn();
|
||||
|
||||
const list = render(
|
||||
<List
|
||||
list={workoutList}
|
||||
<ListTest
|
||||
list={workouts}
|
||||
selected={selected}
|
||||
setSelected={setSelected}
|
||||
type="radio"
|
||||
|
@ -77,8 +70,8 @@ describe('The Search Bar', () => {
|
|||
const setSelected = jest.fn();
|
||||
|
||||
const list = render(
|
||||
<List
|
||||
list={workoutList}
|
||||
<ListTest
|
||||
list={workouts}
|
||||
selected={selected}
|
||||
setSelected={setSelected}
|
||||
type="radio"
|
||||
|
@ -94,10 +87,10 @@ describe('The Search Bar', () => {
|
|||
const items = list.getAllByRole('radio');
|
||||
|
||||
// Expect only 'Push Workout' and 'Pull Workout to remain
|
||||
expect(items.length).toBe(4);
|
||||
expect(items[0].getAttribute('value') === 'Push Workout').toBeTruthy();
|
||||
expect(items[1].getAttribute('value') === 'Pull Workout').toBeTruthy();
|
||||
expect(items[2].getAttribute('value') === 'Legs Workout').toBeTruthy();
|
||||
expect(items[3].getAttribute('value') === 'Upper Workout').toBeTruthy();
|
||||
expect(items.length).toBe(8);
|
||||
expect(items[4].getAttribute('value') === 'Push Workout').toBeTruthy();
|
||||
expect(items[5].getAttribute('value') === 'Pull Workout').toBeTruthy();
|
||||
expect(items[6].getAttribute('value') === 'Legs Workout').toBeTruthy();
|
||||
expect(items[7].getAttribute('value') === 'Upper Workout').toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -0,0 +1,133 @@
|
|||
// Import React
|
||||
import React, { useEffect, useState } from 'react';
|
||||
|
||||
// Firebase
|
||||
import { collection, doc, updateDoc } from 'firebase/firestore';
|
||||
import { db } from '../../firebase-config';
|
||||
|
||||
// Authentication
|
||||
import { useAuth } from '../../context/authUserContext';
|
||||
|
||||
// Styles
|
||||
import styles from '../../styles/List.module.css';
|
||||
|
||||
// Get reference to users collection
|
||||
const usersCollectionRef = collection(db, 'users');
|
||||
|
||||
const star = '/images/star.png';
|
||||
const starFilled = '/images/starFilled.png';
|
||||
|
||||
// Element returns what should be displayed for each element of the list
|
||||
export default function Element({ element, type }) {
|
||||
// State of the image that is displayed as the favourite button
|
||||
const [imgPath, setImgPath] = useState(star);
|
||||
|
||||
const { authUser } = useAuth();
|
||||
|
||||
useEffect(() => {
|
||||
const isChecked = () => {
|
||||
if (authUser) {
|
||||
if (type === 'workouts') {
|
||||
if (authUser.favouriteWorkouts.includes(element.id)) {
|
||||
setImgPath(starFilled);
|
||||
} else {
|
||||
setImgPath(star);
|
||||
}
|
||||
}
|
||||
if (type === 'exercises') {
|
||||
if (authUser.favouriteExercises.includes(element.id)) {
|
||||
setImgPath(starFilled);
|
||||
} else {
|
||||
setImgPath(star);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
setImgPath(star);
|
||||
}
|
||||
};
|
||||
isChecked();
|
||||
}, [authUser, element.id, type]);
|
||||
|
||||
const updateFavWorkouts = async (newFavs) => {
|
||||
authUser.favouriteWorkouts = newFavs;
|
||||
const docRef = doc(usersCollectionRef, authUser.uid);
|
||||
await updateDoc(docRef, {
|
||||
favouriteWorkouts: newFavs,
|
||||
});
|
||||
};
|
||||
|
||||
const updateFavExercises = async (newFavs) => {
|
||||
authUser.favouriteExercises = newFavs;
|
||||
const docRef = doc(usersCollectionRef, authUser.uid);
|
||||
await updateDoc(docRef, {
|
||||
favouriteExercises: newFavs,
|
||||
});
|
||||
};
|
||||
|
||||
const removeFavWorkout = (elem) => {
|
||||
const newFavs = authUser.favouriteWorkouts.filter((val) => val !== elem);
|
||||
updateFavWorkouts(newFavs);
|
||||
};
|
||||
|
||||
const addToFavWorkouts = async (elem) => {
|
||||
authUser.favouriteWorkouts.push(elem);
|
||||
updateFavWorkouts(authUser.favouriteWorkouts);
|
||||
};
|
||||
|
||||
const removeFavExercise = async (elem) => {
|
||||
const newFavs = authUser.favouriteExercises.filter((val) => val !== elem);
|
||||
updateFavExercises(newFavs);
|
||||
};
|
||||
|
||||
const addToFavExercises = async (elem) => {
|
||||
authUser.favouriteExercises.push(elem);
|
||||
updateFavExercises(authUser.favouriteExercises);
|
||||
};
|
||||
|
||||
// Event handler if the favourite button is clicked on
|
||||
const toggleStar = (e) => {
|
||||
e.preventDefault();
|
||||
if (authUser) {
|
||||
if (type === 'workouts') {
|
||||
if (authUser.favouriteWorkouts.includes(element.id)) {
|
||||
removeFavWorkout(element.id);
|
||||
setImgPath(star);
|
||||
} else {
|
||||
addToFavWorkouts(element.id);
|
||||
setImgPath(starFilled);
|
||||
}
|
||||
}
|
||||
if (type === 'exercises') {
|
||||
if (authUser.favouriteExercises.includes(element.id)) {
|
||||
removeFavExercise(element.id);
|
||||
setImgPath(star);
|
||||
} else {
|
||||
addToFavExercises(element.id);
|
||||
setImgPath(starFilled);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={styles.selement}>
|
||||
<div className={styles.stxt}>
|
||||
<h1>{element.name}</h1>
|
||||
<p>{element.muscleGroups.join(', ')}</p>
|
||||
</div>
|
||||
|
||||
<div className={styles.star}>
|
||||
<form>
|
||||
<input
|
||||
type="image"
|
||||
src={imgPath}
|
||||
height={38}
|
||||
width={38}
|
||||
alt="star"
|
||||
onClick={toggleStar}
|
||||
/>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -16,6 +16,7 @@ import SignInView from '../Profile/SignInView';
|
|||
// Styles
|
||||
import styles from '../../styles/Navbar.module.css';
|
||||
|
||||
// User Authentication
|
||||
import { useAuth } from '../../context/authUserContext';
|
||||
|
||||
export default function TopNavbar() {
|
||||
|
|
|
@ -3,7 +3,7 @@ import React, { useState } from 'react';
|
|||
|
||||
// Bootstrap Components
|
||||
import Nav from 'react-bootstrap/Nav';
|
||||
import Figure from 'react-bootstrap/Figure';
|
||||
import Image from 'react-bootstrap/Image';
|
||||
import Offcanvas from 'react-bootstrap/Offcanvas';
|
||||
|
||||
// Next Components
|
||||
|
@ -13,7 +13,7 @@ import Link from 'next/link';
|
|||
import { useAuth } from '../../context/authUserContext';
|
||||
|
||||
// Custom Components
|
||||
import SettingsView from './SettingsView';
|
||||
// import SettingsView from './SettingsView';
|
||||
|
||||
// Styles
|
||||
import styles from '../../styles/Profile.module.css';
|
||||
|
@ -34,7 +34,7 @@ export default function ProfileView() {
|
|||
<>
|
||||
<Nav onClick={handleShow}>Profile</Nav>
|
||||
|
||||
<Offcanvas show={show} onHide={handleClose}>
|
||||
<Offcanvas show={show} onHide={handleClose} placement="end">
|
||||
<Offcanvas.Header closeButton>
|
||||
<Offcanvas.Title>Profile</Offcanvas.Title>
|
||||
</Offcanvas.Header>
|
||||
|
@ -43,35 +43,28 @@ export default function ProfileView() {
|
|||
<div className={styles.container}>
|
||||
<main className={styles.main}>
|
||||
<h1>{authUser.name}</h1>
|
||||
<Figure>
|
||||
<Figure.Image
|
||||
width={200}
|
||||
height={200}
|
||||
alt="200x200"
|
||||
src={authUser.photoURL}
|
||||
/>
|
||||
</Figure>
|
||||
<Image
|
||||
src={authUser.photoURL}
|
||||
width={150}
|
||||
height={150}
|
||||
roundedCircle="true"
|
||||
/>
|
||||
|
||||
<div className={styles.grid}>
|
||||
<div className={styles.card}>
|
||||
<Link href="/" className={styles.card}>
|
||||
<p>Your favourites</p>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div className={styles.card}>
|
||||
<Link href="/workouts" className={styles.card}>
|
||||
<p>Your workouts</p>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
{/* Waiting for email/pword sign in */}
|
||||
{/* <div>
|
||||
<Nav.Link className={styles.card}>
|
||||
<p>
|
||||
<SettingsView />
|
||||
</p>
|
||||
</Nav.Link>
|
||||
</div>
|
||||
</div> */}
|
||||
|
||||
<div>
|
||||
<Nav.Link className={styles.card} onClick={handleSignOut}>
|
||||
|
|
|
@ -6,14 +6,20 @@ import Nav from 'react-bootstrap/Nav';
|
|||
import Offcanvas from 'react-bootstrap/Offcanvas';
|
||||
import Button from 'react-bootstrap/Button';
|
||||
import Form from 'react-bootstrap/Form';
|
||||
import Image from 'react-bootstrap/Image';
|
||||
import { FloatingLabel } from 'react-bootstrap';
|
||||
|
||||
// Firebase
|
||||
import { useAuth } from '../../context/authUserContext';
|
||||
|
||||
// Styles
|
||||
import styles from '../../styles/Settings.module.css';
|
||||
|
||||
export default function SettingsView() {
|
||||
const [show, setShow] = useState(false);
|
||||
|
||||
const { authUser } = useAuth();
|
||||
|
||||
const handleClose = () => setShow(false);
|
||||
const handleShow = () => setShow(true);
|
||||
|
||||
|
@ -21,7 +27,7 @@ export default function SettingsView() {
|
|||
<>
|
||||
<Nav onClick={handleShow}>Settings</Nav>
|
||||
|
||||
<Offcanvas show={show} onHide={handleClose}>
|
||||
<Offcanvas show={show} onHide={handleClose} placement="end">
|
||||
<Offcanvas.Header closeButton>
|
||||
<Offcanvas.Title>Settings</Offcanvas.Title>
|
||||
</Offcanvas.Header>
|
||||
|
@ -29,11 +35,20 @@ export default function SettingsView() {
|
|||
<div className={styles.container}>
|
||||
<main className={styles.main}>
|
||||
<Form>
|
||||
<h3>
|
||||
<Form.Label>Change Name</Form.Label>
|
||||
</h3>
|
||||
<h4>
|
||||
<Form.Label>Update Profile Picture</Form.Label>
|
||||
</h4>
|
||||
<div className={styles.form}>
|
||||
<Image src={authUser.photoURL} roundedCircle="true" />
|
||||
<Form.Group controlId="formFile" className="mb-3">
|
||||
<Form.Control type="file" size="sm" />
|
||||
</Form.Group>
|
||||
</div>
|
||||
|
||||
<div className={styles.item}>
|
||||
<h4>
|
||||
<Form.Label>Change Name</Form.Label>
|
||||
</h4>
|
||||
<div className={styles.form}>
|
||||
<FloatingLabel label="First Name" className="mb-3">
|
||||
<Form.Control placeholder="Alice" />
|
||||
</FloatingLabel>
|
||||
|
@ -42,11 +57,10 @@ export default function SettingsView() {
|
|||
</FloatingLabel>
|
||||
</div>
|
||||
|
||||
<h3>
|
||||
<h4>
|
||||
<Form.Label>Change Password</Form.Label>
|
||||
</h3>
|
||||
|
||||
<div className={styles.item}>
|
||||
</h4>
|
||||
<div className={styles.form}>
|
||||
<FloatingLabel label="Enter New Password" className="mb-3">
|
||||
<Form.Control type="password" />
|
||||
</FloatingLabel>
|
||||
|
@ -55,9 +69,9 @@ export default function SettingsView() {
|
|||
</FloatingLabel>
|
||||
</div>
|
||||
|
||||
<div className={styles.item}>
|
||||
<div className={styles.form}>
|
||||
<Button variant="primary" type="submit" onClick={handleClose}>
|
||||
Confirm
|
||||
Save Changes
|
||||
</Button>
|
||||
</div>
|
||||
</Form>
|
||||
|
|
|
@ -34,7 +34,7 @@ export default function SignInView() {
|
|||
<>
|
||||
<Nav onClick={handleShow}>Sign in</Nav>
|
||||
|
||||
<Offcanvas show={show} onHide={handleClose}>
|
||||
<Offcanvas show={show} onHide={handleClose} placement="end">
|
||||
<Offcanvas.Header closeButton>
|
||||
<Offcanvas.Title />
|
||||
</Offcanvas.Header>
|
||||
|
@ -63,15 +63,17 @@ export default function SignInView() {
|
|||
<Form.Label>Password</Form.Label>
|
||||
<Form.Control
|
||||
type="password"
|
||||
name="email"
|
||||
name="password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
placeholder="Password"
|
||||
/>
|
||||
</Form.Group>
|
||||
<Form.Group className="mb-3" controlId="formBasicCheckbox">
|
||||
|
||||
{/* Waiting for email/pword sign in */}
|
||||
{/* <Form.Group className="mb-3" controlId="formBasicCheckbox">
|
||||
<Form.Check type="checkbox" label="Remember me" />
|
||||
</Form.Group>
|
||||
</Form.Group> */}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -0,0 +1,55 @@
|
|||
// React
|
||||
import React, { useState } from 'react';
|
||||
|
||||
// Next components
|
||||
import Image from 'next/image';
|
||||
|
||||
// Styles
|
||||
import styles from '../../styles/List.module.css';
|
||||
|
||||
const star = '/images/star.png';
|
||||
const starFilled = '/images/starFilled.png';
|
||||
|
||||
export default function ElementTest({ element }) {
|
||||
// State of the image that is displayed as the favourite button
|
||||
const [imgPath, setImgPath] = useState(star);
|
||||
|
||||
// Event handler if the favourite button is clicked on
|
||||
const toggleStar = (e) => {
|
||||
e.preventDefault();
|
||||
if (imgPath === star) {
|
||||
setImgPath(starFilled);
|
||||
} else {
|
||||
setImgPath(star);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={styles.element}>
|
||||
<Image
|
||||
src={element.imgSrc}
|
||||
alt={element.imgAlt}
|
||||
height={84}
|
||||
width={120}
|
||||
/>
|
||||
|
||||
<div className={styles.txt}>
|
||||
<h1>{element.name}</h1>
|
||||
<p>{element.muscleGroups.join(', ')}</p>
|
||||
</div>
|
||||
|
||||
<div className={styles.star}>
|
||||
<form>
|
||||
<input
|
||||
type="image"
|
||||
src={imgPath}
|
||||
height={38}
|
||||
width={38}
|
||||
alt="star"
|
||||
onClick={toggleStar}
|
||||
/>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,69 @@
|
|||
// React
|
||||
import React, { useState } from 'react';
|
||||
|
||||
// Bootstrap components
|
||||
import ToggleButton from 'react-bootstrap/ToggleButton';
|
||||
import ToggleButtonGroup from 'react-bootstrap/ToggleButtonGroup';
|
||||
|
||||
// Custom components
|
||||
import SearchFilterBar from '../List/SearchFilterBar';
|
||||
import ElementTest from './ElementTest';
|
||||
import styles from '../../styles/List.module.css';
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {*} list A list of either workouts or exercises
|
||||
* @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
|
||||
* @returns
|
||||
*/
|
||||
export default function List({ list, listType, selected, setSelected, type }) {
|
||||
// A function to handle when a new element is selected
|
||||
const handleChange = (e) => {
|
||||
setSelected(e);
|
||||
};
|
||||
|
||||
// State to keep track of the search input
|
||||
const [searchInput, setSearchInput] = useState('');
|
||||
|
||||
// When searchInput is changed, filteredList updates to only contain elements with names including searchInput
|
||||
const filteredList = list.filter((item) => {
|
||||
if (searchInput === '') {
|
||||
return item;
|
||||
}
|
||||
return item.name.toLowerCase().includes(searchInput);
|
||||
});
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<SearchFilterBar
|
||||
searchInput={searchInput}
|
||||
setSearchInput={setSearchInput}
|
||||
/>
|
||||
|
||||
{/* The list. A group of toggle buttons, so that the active one can be kept track of */}
|
||||
<div className={styles.scrollableContainer}>
|
||||
<ToggleButtonGroup
|
||||
type={listType}
|
||||
value={selected}
|
||||
onChange={handleChange}
|
||||
vertical
|
||||
name="button-list"
|
||||
>
|
||||
{filteredList.map((element) => (
|
||||
<ToggleButton
|
||||
key={element.name}
|
||||
id={`${listType}-${element.name}`}
|
||||
variant="light"
|
||||
name={listType}
|
||||
value={element.name}
|
||||
>
|
||||
<ElementTest element={element} type={type} />
|
||||
</ToggleButton>
|
||||
))}
|
||||
</ToggleButtonGroup>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,43 @@
|
|||
// Import React
|
||||
import React from 'react';
|
||||
|
||||
// Next components
|
||||
import Image from 'next/image';
|
||||
import { Container, Row, Col } from 'react-bootstrap';
|
||||
|
||||
// Styles
|
||||
import styles from '../../styles/Workouts/WorkoutList.module.css';
|
||||
|
||||
// Element returns what should be displayed for each element of the list
|
||||
export default function WorkoutElement({ element }) {
|
||||
// console.log(element)
|
||||
return (
|
||||
<Container className={styles.workoutexerciselist}>
|
||||
<Col>
|
||||
<div className={styles.element}>
|
||||
<Image
|
||||
src={element.imgSrc}
|
||||
alt={element.imgAlt}
|
||||
height={40}
|
||||
width={65}
|
||||
/>
|
||||
<div className={styles.txt}>
|
||||
<p>{element.name}</p>
|
||||
</div>
|
||||
</div>
|
||||
</Col>
|
||||
<Col>
|
||||
<Row>
|
||||
<div className={styles.sr}>
|
||||
<p>{element.sets} sets</p>
|
||||
</div>
|
||||
</Row>
|
||||
<Row>
|
||||
<div className={styles.sr}>
|
||||
<p>{element.reps} reps</p>
|
||||
</div>
|
||||
</Row>
|
||||
</Col>
|
||||
</Container>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,39 @@
|
|||
// React
|
||||
import React from 'react';
|
||||
|
||||
// Bootstrap Components
|
||||
import Button from 'react-bootstrap/Button';
|
||||
import ButtonGroup from 'react-bootstrap/ButtonGroup';
|
||||
|
||||
// Custom components
|
||||
import WorkoutElement from './WorkoutElement';
|
||||
|
||||
// Styles
|
||||
import styles from '../../styles/Workouts/WorkoutList.module.css';
|
||||
|
||||
export default function WorkoutList({ exerciseList }) {
|
||||
return (
|
||||
<div>
|
||||
{/* The list. A group of toggle buttons, so that the active one can be kept track of */}
|
||||
<div className={styles.scrollableContainer}>
|
||||
<ButtonGroup
|
||||
// onChange={onchange}
|
||||
variant="secondary"
|
||||
vertical
|
||||
name="button-list"
|
||||
>
|
||||
{exerciseList.map((element) => (
|
||||
<Button
|
||||
className={styles.list}
|
||||
key={element.id}
|
||||
id={`radio-${element.id}`}
|
||||
value={element.id}
|
||||
>
|
||||
<WorkoutElement element={element} />
|
||||
</Button>
|
||||
))}
|
||||
</ButtonGroup>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -1,13 +1,21 @@
|
|||
// React
|
||||
import React from 'react';
|
||||
|
||||
export default function YouTube() {
|
||||
return (
|
||||
<iframe
|
||||
width="100%"
|
||||
height="60%"
|
||||
src="https://www.youtube.com/embed/TwD-YGVP4Bk"
|
||||
title="Exercise video"
|
||||
/>
|
||||
);
|
||||
function verifyURL(link) {
|
||||
const regExp =
|
||||
/^.*((youtu.be\/)|(v\/)|(\/u\/\w\/)|(embed\/)|(watch\?))\??v?=?([^#&?]*).*/;
|
||||
const match = link.match(regExp);
|
||||
return match && match[7].length === 11 ? match[7] : false;
|
||||
}
|
||||
|
||||
export default function YouTube({ link }) {
|
||||
if (link) {
|
||||
return (
|
||||
<iframe
|
||||
width="100%"
|
||||
height="60%"
|
||||
src={`https://www.youtube.com/embed/${verifyURL(link)}`}
|
||||
title="Exercise video"
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
// React
|
||||
import { createContext, useContext } from 'react';
|
||||
import React, { createContext, useContext } from 'react';
|
||||
|
||||
// Firebase
|
||||
import useFirebaseAuth from '../lib/useFirebaseAuth';
|
||||
|
|
|
@ -1,14 +1,7 @@
|
|||
// import firebase SDK
|
||||
import { initializeApp } from 'firebase/app';
|
||||
import {
|
||||
getFirestore,
|
||||
getDoc,
|
||||
doc,
|
||||
collection,
|
||||
setDoc,
|
||||
} from 'firebase/firestore';
|
||||
import { getAuth, signInWithRedirect, getRedirectResult } from 'firebase/auth';
|
||||
import { GoogleAuthProvider } from 'firebase/auth';
|
||||
import { getFirestore } from 'firebase/firestore';
|
||||
import { getAuth, GoogleAuthProvider } from 'firebase/auth';
|
||||
|
||||
// store configuration details
|
||||
const firebaseConfig = {
|
||||
|
@ -26,44 +19,3 @@ const app = initializeApp(firebaseConfig);
|
|||
export const db = getFirestore();
|
||||
export const auth = getAuth(app);
|
||||
export const provider = new GoogleAuthProvider();
|
||||
const usersCollectionRef = collection(db, 'users');
|
||||
|
||||
// Firebase features
|
||||
export const signInWithGoogle = async () => {
|
||||
signInWithRedirect(auth, provider);
|
||||
getRedirectResult(auth)
|
||||
.then(async (result) => {
|
||||
// This gives you a Google Access Token.
|
||||
const credential = GoogleAuthProvider.credentialFromResult(result);
|
||||
const token = credential.accessToken;
|
||||
console.log(result);
|
||||
|
||||
// The signed-in user info.
|
||||
const user = result.user;
|
||||
const docRef = doc(db, usersCollectionRef, user.uid);
|
||||
const userDoc = await getDoc(docRef);
|
||||
if (!userDoc.exists()) {
|
||||
createNewUser(user);
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
// Handle Errors here.
|
||||
const errorCode = error.code;
|
||||
const errorMessage = error.message;
|
||||
|
||||
// The email of the user's account used.
|
||||
const email = error.customData.email;
|
||||
|
||||
// The AuthCredential type that was used.
|
||||
const credential = GoogleAuthProvider.credentialFromError(error);
|
||||
// ...
|
||||
});
|
||||
};
|
||||
|
||||
const createNewUser = async (user) => {
|
||||
await setDoc(doc(db, usersCollectionRef, user.uid), {
|
||||
name: user.displayName,
|
||||
photo: user.photoURL,
|
||||
role: 1,
|
||||
});
|
||||
};
|
||||
|
|
|
@ -11,11 +11,14 @@ import { auth, provider, db } from '../firebase-config';
|
|||
|
||||
const usersCollectionRef = collection(db, 'users');
|
||||
|
||||
const formatAuthUser = (user) => ({
|
||||
const formatAuthUser = (user, id) => ({
|
||||
uid: id,
|
||||
name: user.name,
|
||||
email: user.email,
|
||||
photoURL: user.photo,
|
||||
role: user.role,
|
||||
favouriteWorkouts: user.favouriteWorkouts,
|
||||
favouriteExercises: user.favouriteExercises,
|
||||
});
|
||||
|
||||
const createNewUser = async (user) => {
|
||||
|
@ -24,6 +27,8 @@ const createNewUser = async (user) => {
|
|||
email: user.email,
|
||||
photo: user.photoURL,
|
||||
role: 1,
|
||||
favouriteWorkouts: [],
|
||||
favouriteExercises: [],
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -44,7 +49,7 @@ export default function useFirebaseAuth() {
|
|||
createNewUser(user);
|
||||
userDoc = await getDoc(docRef);
|
||||
}
|
||||
const formattedUser = formatAuthUser(userDoc.data());
|
||||
const formattedUser = formatAuthUser(userDoc.data(), user.uid);
|
||||
setAuthUser(formattedUser);
|
||||
setLoading(false);
|
||||
};
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -14,7 +14,7 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"bootstrap": "^5.2.1",
|
||||
"firebase": "^9.10.0",
|
||||
"firebase": "^9.12.1",
|
||||
"next": "12.3.0",
|
||||
"prop-types": "^15.8.1",
|
||||
"react": "18.2.0",
|
||||
|
|
|
@ -0,0 +1,37 @@
|
|||
// React
|
||||
import React, { useEffect, useState } from 'react';
|
||||
|
||||
// Bootstrap components
|
||||
import ListTest from '../components/Test/ListTest';
|
||||
|
||||
// Styles
|
||||
import styles from '../styles/Workouts/Workouts.module.css';
|
||||
|
||||
// Authentication
|
||||
import { exercises } from '../testData/testData';
|
||||
|
||||
export default function ExercisesPage() {
|
||||
const [exerciseList, setExerciseList] = useState([]);
|
||||
useEffect(() => {
|
||||
const getExercises = () => {
|
||||
setExerciseList(exercises);
|
||||
};
|
||||
getExercises();
|
||||
}, []);
|
||||
|
||||
const [selected, setSelected] = useState('');
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<main className={styles.main}>
|
||||
<ListTest
|
||||
list={exerciseList}
|
||||
listType="radio"
|
||||
selected={selected}
|
||||
setSelected={setSelected}
|
||||
type="workouts"
|
||||
/>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,37 @@
|
|||
// React
|
||||
import React, { useEffect, useState } from 'react';
|
||||
|
||||
// Bootstrap components
|
||||
import ListTest from '../components/Test/ListTest';
|
||||
|
||||
// Styles
|
||||
import styles from '../styles/Workouts/Workouts.module.css';
|
||||
|
||||
// Authentication
|
||||
import { workouts } from '../testData/testData';
|
||||
|
||||
export default function WorkoutsPage() {
|
||||
const [workoutList, setWorkoutList] = useState([]);
|
||||
useEffect(() => {
|
||||
const getWorkouts = () => {
|
||||
setWorkoutList(workouts);
|
||||
};
|
||||
getWorkouts();
|
||||
}, []);
|
||||
|
||||
const [selected, setSelected] = useState('');
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<main className={styles.main}>
|
||||
<ListTest
|
||||
list={workoutList}
|
||||
listType="radio"
|
||||
selected={selected}
|
||||
setSelected={setSelected}
|
||||
type="workouts"
|
||||
/>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -1,50 +1,9 @@
|
|||
// React
|
||||
import React, { useState } from 'react';
|
||||
import React from 'react';
|
||||
import { fireEvent, render, screen } from '@testing-library/react';
|
||||
|
||||
// Custom components
|
||||
import List from '../components/List/List';
|
||||
|
||||
// Styles
|
||||
import styles from '../styles/Exercises.module.css';
|
||||
|
||||
// Import the Exercise class so that we can create a dummy set of exercises to render
|
||||
import Exercise from '../public/classes/Exercise';
|
||||
|
||||
function ExercisesTest() {
|
||||
// A dummy exercise list so that we have data to render.
|
||||
// Once the database is implemented this will not be necessary
|
||||
const exerciseList = [];
|
||||
exerciseList.push(
|
||||
new Exercise('Bench Press', ['Chest', 'Shoulder', 'Triceps']),
|
||||
new Exercise('Squats', ['Quadriceps', 'Hamstrings', 'Calves', 'Glutes']),
|
||||
new Exercise('Plank', [
|
||||
'Quadriceps',
|
||||
'Hamstrings',
|
||||
'Core',
|
||||
'Triceps',
|
||||
'Glutes',
|
||||
]),
|
||||
new Exercise('Bench Dips ', ['Chest', 'Triceps']),
|
||||
new Exercise('Lunges', ['Hamstrings', 'Glutes', 'Quadriceps', 'Calves']),
|
||||
new Exercise('Custom exercise 1', ['Back', 'Biceps', 'Abs']),
|
||||
new Exercise('Custom exercise 2', ['Quadriceps', 'Hamstrings', 'Calves']),
|
||||
new Exercise('Custom exercise 3', ['Chest', 'Back', 'Shoulder', 'Triceps'])
|
||||
);
|
||||
|
||||
const selectState = {};
|
||||
[selectState.selected, selectState.setSelected] = useState();
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={styles.container}>
|
||||
<main className={styles.main}>
|
||||
<List list={exerciseList} {...selectState} />
|
||||
</main>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
// Custom Components
|
||||
import ExercisesTest from './ExercisesTest';
|
||||
|
||||
describe('The List of buttons displaying Workouts or Exercises', () => {
|
||||
it('Updates the selected button when another is clicked', () => {
|
||||
|
|
|
@ -1,44 +1,9 @@
|
|||
// React
|
||||
import React, { useState } from 'react';
|
||||
import React from 'react';
|
||||
import { fireEvent, render, screen } from '@testing-library/react';
|
||||
|
||||
// Custom Components
|
||||
//import WorkoutsPage from '../pages/workouts/index';
|
||||
|
||||
import Workout from '../public/classes/Workout';
|
||||
|
||||
import List from '../components/List/List';
|
||||
|
||||
import styles from '../styles/Workouts.module.css';
|
||||
|
||||
function WorkoutsTest() {
|
||||
// A dummy workout list so that we have data to render.
|
||||
// Once the database is implemented this will not be necessary
|
||||
const workoutList = [];
|
||||
workoutList.push(
|
||||
new Workout('Push Workout', ['Chest', 'Shoulder', 'Triceps']),
|
||||
new Workout('Pull Workout', ['Back', 'Biceps', 'Abs']),
|
||||
new Workout('Legs Workout', ['Quadriceps', 'Hamstrings', 'Calves']),
|
||||
new Workout('Upper Workout', ['Chest', 'Back', 'Shoulder', 'Triceps']),
|
||||
new Workout('Workout 1', ['Chest', 'Shoulder', 'Triceps']),
|
||||
new Workout('Workout 2', ['Back', 'Biceps', 'Abs']),
|
||||
new Workout('Workout 3', ['Quadriceps', 'Hamstrings', 'Calves']),
|
||||
new Workout('Workout 4', ['Chest', 'Back', 'Shoulder', 'Triceps'])
|
||||
);
|
||||
|
||||
const selectState = {};
|
||||
[selectState.selected, selectState.setSelected] = useState('');
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={styles.container}>
|
||||
<main className={styles.main}>
|
||||
<List list={workoutList} listType="radio" {...selectState} />
|
||||
</main>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
import WorkoutsTest from './WorkoutsTest';
|
||||
|
||||
describe('The List of buttons displaying Workouts or Exercises', () => {
|
||||
it('Updates the selected button when another is clicked', () => {
|
||||
|
|
|
@ -1,47 +1,105 @@
|
|||
/* eslint-disable react/jsx-props-no-spreading */
|
||||
// React
|
||||
import React, { useState, useEffect } from 'react';
|
||||
|
||||
// Bootstrap components
|
||||
import Col from 'react-bootstrap/Col';
|
||||
import Container from 'react-bootstrap/Container';
|
||||
import Row from 'react-bootstrap/Row';
|
||||
|
||||
// Firebase
|
||||
import { getDocs, collection, query, orderBy, limit } from 'firebase/firestore';
|
||||
import { db } from '../../firebase-config';
|
||||
|
||||
// Custom components
|
||||
import CRUDButton from '../../components/CRUDButton/CRUDButton';
|
||||
import Instructions from '../../components/Instructions';
|
||||
import List from '../../components/List/List';
|
||||
import TopNavbar from '../../components/Navbar/Navbar';
|
||||
import YouTube from '../../components/YouTube';
|
||||
|
||||
// Styles
|
||||
import styles from '../../styles/Exercises.module.css';
|
||||
|
||||
// Get reference to workouts collection
|
||||
// Authentication
|
||||
import { useAuth } from '../../context/authUserContext';
|
||||
|
||||
// Get reference to exercises collection
|
||||
const exercisesCollectionRef = collection(db, 'exercises');
|
||||
|
||||
export default function ExercisesPage() {
|
||||
const [exerciseList, setExerciseList] = useState([]);
|
||||
const [selectedExercise, setSelectedExercise] = useState();
|
||||
const { authUser } = useAuth();
|
||||
|
||||
useEffect(() => {
|
||||
const getExercises = async () => {
|
||||
const q = query(exercisesCollectionRef, orderBy('name'), limit(10));
|
||||
const data = await getDocs(q);
|
||||
setExerciseList(
|
||||
data.docs.map((document) => ({ ...document.data(), id: document.id }))
|
||||
);
|
||||
const exercises = data.docs.map((doc) => ({ ...doc.data(), id: doc.id }));
|
||||
|
||||
if (authUser) {
|
||||
const favs = exercises.filter((doc) =>
|
||||
authUser.favouriteExercises.includes(doc.id)
|
||||
);
|
||||
const unfavs = exercises.filter(
|
||||
(doc) => !authUser.favouriteExercises.includes(doc.id)
|
||||
);
|
||||
const finalList = favs.concat(unfavs);
|
||||
setSelectedExercise(finalList[0]);
|
||||
setExerciseList(finalList);
|
||||
} else {
|
||||
setSelectedExercise(exercises[0]);
|
||||
setExerciseList(exercises);
|
||||
}
|
||||
};
|
||||
getExercises();
|
||||
}, []);
|
||||
}, [authUser]);
|
||||
|
||||
const selectState = {};
|
||||
[selectState.selected, selectState.setSelected] = useState();
|
||||
const [selected, setSelected] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
const getSelected = () => {
|
||||
if (selected) {
|
||||
exerciseList.forEach((doc) => {
|
||||
if (doc.id === selected) {
|
||||
setSelectedExercise(doc);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
getSelected();
|
||||
}, [selected, exerciseList]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<TopNavbar />
|
||||
<div className={styles.container}>
|
||||
<main className={styles.main}>
|
||||
<CRUDButton type="exercise" create />
|
||||
<List list={exerciseList} listType="radio" {...selectState} />
|
||||
</main>
|
||||
</div>
|
||||
<Container className={styles.container}>
|
||||
<CRUDButton type="exercise" create />
|
||||
<Row>
|
||||
<Col>
|
||||
{selectedExercise != null && (
|
||||
<YouTube link={selectedExercise.videoURL} />
|
||||
)}
|
||||
{selectedExercise != null && (
|
||||
<Instructions text={selectedExercise.instructions} />
|
||||
)}
|
||||
</Col>
|
||||
|
||||
<Col>
|
||||
<div>
|
||||
<main className={styles.main}>
|
||||
<List
|
||||
list={exerciseList}
|
||||
listType="radio"
|
||||
selected={selected}
|
||||
setSelected={setSelected}
|
||||
type="exercises"
|
||||
/>
|
||||
</main>
|
||||
</div>
|
||||
</Col>
|
||||
</Row>
|
||||
</Container>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,50 +1,125 @@
|
|||
// 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';
|
||||
|
||||
// Next components
|
||||
import Image from 'next/image';
|
||||
|
||||
// Firebase
|
||||
import { getDocs, collection, query, orderBy, limit } from 'firebase/firestore';
|
||||
import { db } from '../../firebase-config';
|
||||
|
||||
// Bootstrap components
|
||||
// Custom components
|
||||
import CRUDButton from '../../components/CRUDButton/CRUDButton';
|
||||
import List from '../../components/List/List';
|
||||
import TopNavbar from '../../components/Navbar/Navbar';
|
||||
import WorkoutList from '../../components/WorkoutList/WorkoutList';
|
||||
|
||||
// Styles
|
||||
import styles from '../../styles/Workouts.module.css';
|
||||
import styles from '../../styles/Workouts/Workouts.module.css';
|
||||
|
||||
// Import the database reference and functions for reading from firestore
|
||||
// Authentication
|
||||
import { useAuth } from '../../context/authUserContext';
|
||||
|
||||
// Get reference to workouts collection
|
||||
const workoutsCollectionRef = collection(db, 'workouts');
|
||||
|
||||
export default function WorkoutsPage() {
|
||||
const [workoutList, setWorkoutList] = useState([]);
|
||||
const { authUser } = useAuth();
|
||||
|
||||
const [selected, setSelected] = useState('');
|
||||
const [selectedWorkout, setSelectedWorkout] = useState();
|
||||
|
||||
useEffect(() => {
|
||||
const getWorkouts = async () => {
|
||||
const q = query(workoutsCollectionRef, orderBy('name'), limit(10));
|
||||
const data = await getDocs(q);
|
||||
setWorkoutList(data.docs.map((doc) => ({ ...doc.data(), id: doc.id })));
|
||||
const workouts = data.docs.map((doc) => ({ ...doc.data(), id: doc.id }));
|
||||
|
||||
if (authUser) {
|
||||
const favs = workouts.filter((doc) =>
|
||||
authUser.favouriteWorkouts.includes(doc.id)
|
||||
);
|
||||
const unfavs = workouts.filter(
|
||||
(doc) => !authUser.favouriteWorkouts.includes(doc.id)
|
||||
);
|
||||
const finalList = favs.concat(unfavs);
|
||||
setSelectedWorkout(finalList[0]);
|
||||
setWorkoutList(finalList);
|
||||
} else {
|
||||
setSelectedWorkout(workouts[0]);
|
||||
setWorkoutList(workouts);
|
||||
}
|
||||
};
|
||||
getWorkouts();
|
||||
}, []);
|
||||
}, [authUser]);
|
||||
|
||||
const [selected, setSelected] = useState('');
|
||||
useEffect(() => {
|
||||
const getSelected = () => {
|
||||
workoutList.forEach((doc) => {
|
||||
if (doc.id === selected) {
|
||||
setSelectedWorkout(doc);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
getSelected();
|
||||
}, [selected, workoutList]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<TopNavbar />
|
||||
<div className={styles.container}>
|
||||
<main className={styles.main}>
|
||||
<CRUDButton type="workout" create />
|
||||
<List
|
||||
list={workoutList}
|
||||
listType="radio"
|
||||
selected={selected}
|
||||
setSelected={setSelected}
|
||||
/>
|
||||
</main>
|
||||
</div>
|
||||
<Container className={styles.container}>
|
||||
<CRUDButton type="workout" create />
|
||||
<Row>
|
||||
<Col>
|
||||
{selectedWorkout != null && (
|
||||
<div className={styles.imgcontainer}>
|
||||
<Image
|
||||
src={selectedWorkout.imgSrc}
|
||||
alt="workout image"
|
||||
width="0"
|
||||
height="0"
|
||||
sizes="100vw"
|
||||
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>
|
||||
<div>
|
||||
<main className={styles.main}>
|
||||
<List
|
||||
list={workoutList}
|
||||
listType="radio"
|
||||
selected={selected}
|
||||
setSelected={setSelected}
|
||||
type="workouts"
|
||||
/>
|
||||
</main>
|
||||
</div>
|
||||
</Col>
|
||||
</Row>
|
||||
</Container>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -2,13 +2,15 @@ export default class Exercise {
|
|||
/**
|
||||
* A class to represent Exercise.
|
||||
* @param {String} name
|
||||
* @param {[String]} muscles
|
||||
* @param {[String]} muscleGroups
|
||||
* @param {{Sets: Int, Reps: Int}} repRange
|
||||
*/
|
||||
constructor(name, muscles) {
|
||||
constructor(name, muscleGroups, repRange) {
|
||||
this.name = name;
|
||||
this.muscles = muscles;
|
||||
this.muscleGroups = muscleGroups;
|
||||
this.repRange = repRange;
|
||||
this.imgSrc =
|
||||
'https://images.pexels.com/photos/3837781/pexels-photo-3837781.jpeg?auto=compress&cs=tinysrgb&w=1260&h=750&dpr=1';
|
||||
'https://images.pexels.com/photos/2204196/pexels-photo-2204196.jpeg';
|
||||
this.imgAlt = 'A man doing a bench press.';
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,12 +3,14 @@ class Workout {
|
|||
* A class to represent Workouts.
|
||||
* @param {String} name
|
||||
* @param {[String]} muscleGroups
|
||||
* * @param {[String]} exercises
|
||||
*/
|
||||
constructor(name, muscleGroups) {
|
||||
constructor(name, muscleGroups, exercises) {
|
||||
this.name = name;
|
||||
this.muscleGroups = muscleGroups;
|
||||
this.exercises = exercises;
|
||||
this.imgSrc =
|
||||
'https://images.pexels.com/photos/3837781/pexels-photo-3837781.jpeg?auto=compress&cs=tinysrgb&w=1260&h=750&dpr=1';
|
||||
'https://images.pexels.com/photos/2204196/pexels-photo-2204196.jpeg';
|
||||
this.imgAlt = 'A man doing a bench press.';
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
.container {
|
||||
padding: 0 2rem;
|
||||
margin: 50px;
|
||||
padding: 15vh 0;
|
||||
margin: 0 auto;
|
||||
align-content: center;
|
||||
}
|
||||
|
||||
.main {
|
||||
min-height: 100vh;
|
||||
padding: 4rem 0;
|
||||
margin: 0 2vw;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
@ -86,12 +86,12 @@
|
|||
max-width: 300px;
|
||||
}
|
||||
|
||||
.card:hover,
|
||||
/* .card:hover,
|
||||
.card:focus,
|
||||
.card:active {
|
||||
color: #0070f3;
|
||||
border-color: #0070f3;
|
||||
}
|
||||
} */
|
||||
|
||||
.card h2 {
|
||||
margin: 0 0 1rem 0;
|
||||
|
|
|
@ -1,11 +1,19 @@
|
|||
.instructions {
|
||||
border: 2px solid black;
|
||||
border: 1px solid black;
|
||||
border-radius: 10px;
|
||||
padding: 0px 20px;
|
||||
margin: 30px 0;
|
||||
margin: 2vh 0;
|
||||
height: 30vh;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow-y: auto;
|
||||
min-height: 0;
|
||||
scrollbar-width: auto;
|
||||
}
|
||||
|
||||
.line {
|
||||
display: inline-block;
|
||||
padding: 0.5em 0;
|
||||
text-align: justify;
|
||||
}
|
||||
|
|
|
@ -1,33 +1,87 @@
|
|||
/* Holds the whole list and search filter bar */
|
||||
.container {
|
||||
margin-top: 3vh;
|
||||
}
|
||||
|
||||
/* Makes the list scrollable if it overflows */
|
||||
.scrollableContainer {
|
||||
border: 1px solid black;
|
||||
border-radius: 10px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 75vh;
|
||||
height: 71vh;
|
||||
overflow-y: auto;
|
||||
min-height: 0;
|
||||
padding: 1.5vw 1.5vw 0.5vw 1.5vw;
|
||||
scrollbar-width: auto;
|
||||
}
|
||||
|
||||
.list {
|
||||
border: 1px solid black;
|
||||
margin-bottom: 1vw;
|
||||
background-color: white;
|
||||
color: black;
|
||||
}
|
||||
|
||||
.list:hover,
|
||||
.list:active,
|
||||
.list:checked {
|
||||
background-color: #0070f3 !important;
|
||||
color: white !important;
|
||||
}
|
||||
|
||||
/* Contains the image and the text */
|
||||
.element {
|
||||
display: flex;
|
||||
|
||||
.element,
|
||||
.selement {
|
||||
display: inline-flex;
|
||||
height: 14vh;
|
||||
width: 25vw + 120px + 60px;
|
||||
padding: 0.5vw;
|
||||
}
|
||||
|
||||
.selement {
|
||||
margin: 0 auto;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.star {
|
||||
margin: auto;
|
||||
display: block;
|
||||
margin-left: 1vw;
|
||||
}
|
||||
|
||||
/* text in list */
|
||||
.txt h1 {
|
||||
font-size: x-large;
|
||||
margin-top: 10px;
|
||||
margin-left: 5px;
|
||||
.txt,
|
||||
.stxt {
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.stxt {
|
||||
text-align: center;
|
||||
text-overflow: ellipsis;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.stxt h1 {
|
||||
font-size: xx-large;
|
||||
margin: 10px 0 0 0;
|
||||
}
|
||||
|
||||
.stxt p {
|
||||
font-size: large;
|
||||
font-style: italic;
|
||||
font-weight: 300;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
/* text in list */
|
||||
.txt {
|
||||
text-align: left;
|
||||
text-overflow: ellipsis;
|
||||
width: 20vw;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
margin-left: 1vw;
|
||||
}
|
||||
|
||||
.txt h1 {
|
||||
font-size: xx-large;
|
||||
margin: 10px 0 0 5px;
|
||||
}
|
||||
|
||||
.txt p {
|
||||
|
@ -36,16 +90,6 @@
|
|||
font-weight: 300;
|
||||
margin-left: 6px;
|
||||
margin-bottom: 0;
|
||||
text-align: left;
|
||||
text-overflow: ellipsis;
|
||||
width: 20vw;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.star {
|
||||
margin-top: 21px;
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 576px) {
|
||||
|
|
|
@ -1,22 +1,19 @@
|
|||
.container {
|
||||
padding-top: 10px;
|
||||
padding-bottom: 10px;
|
||||
/* padding-top: 10px; */
|
||||
padding-bottom: 20px;
|
||||
height: 11vh;
|
||||
}
|
||||
|
||||
.filter {
|
||||
background: #fff;
|
||||
color: black;
|
||||
border: 2px solid lightgray;
|
||||
border-radius: 3px;
|
||||
padding: 5px 10px;
|
||||
width: 100%;
|
||||
.filter,
|
||||
.container input {
|
||||
text-align: center;
|
||||
font-size: 1.5vw;
|
||||
}
|
||||
|
||||
.container input {
|
||||
text-align: center;
|
||||
background-image: url(../public/search.svg);
|
||||
background-size: 8%;
|
||||
background-position: 6vw 7px;
|
||||
background-position: 5.2vw 2.5vh;
|
||||
background-repeat: no-repeat;
|
||||
text-indent: 20px;
|
||||
}
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
|
||||
.main {
|
||||
min-height: 100vh;
|
||||
padding: 4rem 0;
|
||||
padding: 2rem 0;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
@ -109,6 +109,7 @@
|
|||
.buttons {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.signup {
|
||||
|
@ -119,6 +120,12 @@
|
|||
width: 100%;
|
||||
}
|
||||
|
||||
.form {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.grid {
|
||||
width: 100%;
|
||||
|
|
|
@ -0,0 +1,91 @@
|
|||
/* Makes the list scrollable if it overflows */
|
||||
.scrollableContainer {
|
||||
border: 1px solid black;
|
||||
border-radius: 10px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 30vh;
|
||||
overflow-y: auto;
|
||||
min-height: 0;
|
||||
/* padding: 1.5vw 1.5vw 0.5vw 1.5vw; */
|
||||
scrollbar-width: auto;
|
||||
}
|
||||
|
||||
.workoutexerciselist {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.list {
|
||||
border: 0;
|
||||
background-color: white;
|
||||
color: black;
|
||||
}
|
||||
|
||||
.list:hover,
|
||||
.list:active,
|
||||
.list:checked {
|
||||
border: 0 !important;
|
||||
background-color: #adb5bd !important;
|
||||
color: white !important;
|
||||
}
|
||||
|
||||
/* Contains the image and the text */
|
||||
.element {
|
||||
display: flex;
|
||||
width: 35vw;
|
||||
}
|
||||
|
||||
.sr p {
|
||||
margin-left: 0.5vw;
|
||||
font-size: medium;
|
||||
font-weight: 300;
|
||||
margin-bottom: 0;
|
||||
text-align: center;
|
||||
text-overflow: ellipsis;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.txt p {
|
||||
margin-left: 0.5vw;
|
||||
font-size: xx-large;
|
||||
font-weight: 300;
|
||||
margin-bottom: 0;
|
||||
text-align: center;
|
||||
text-overflow: ellipsis;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 576px) {
|
||||
.element {
|
||||
display: flex;
|
||||
width: 80vw;
|
||||
}
|
||||
|
||||
.txt h1 {
|
||||
font-size: x-large;
|
||||
margin-top: 10px;
|
||||
margin-left: 5px;
|
||||
text-align: left;
|
||||
text-overflow: ellipsis;
|
||||
width: 40vw;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.txt p {
|
||||
font-size: large;
|
||||
font-style: italic;
|
||||
font-weight: 300;
|
||||
margin-left: 6px;
|
||||
margin-bottom: 0;
|
||||
text-align: left;
|
||||
text-overflow: ellipsis;
|
||||
width: 40vw;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
|
@ -1,10 +1,12 @@
|
|||
@import url(http://fonts.googleapis.com/css?family=Roboto:400,100,100italic,300,300italic,400italic,500,500italic,700,700italic,900italic,900);
|
||||
|
||||
.container {
|
||||
padding: 0 2rem;
|
||||
padding: 15vh 0;
|
||||
margin: 0 auto;
|
||||
align-content: center;
|
||||
}
|
||||
|
||||
.main {
|
||||
min-height: 100vh;
|
||||
padding: 4rem 0;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
@ -12,6 +14,10 @@
|
|||
align-items: center;
|
||||
}
|
||||
|
||||
.workoutlist {
|
||||
margin-top: 4vh;
|
||||
}
|
||||
|
||||
.footer {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
|
@ -108,6 +114,36 @@
|
|||
margin-left: 0.5rem;
|
||||
}
|
||||
|
||||
.imgcontainer {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* Bottom right text */
|
||||
.textblock {
|
||||
position: absolute;
|
||||
bottom: 20px;
|
||||
left: 20px;
|
||||
color: white;
|
||||
padding-left: 20px;
|
||||
padding-right: 20px;
|
||||
overflow: hidden;
|
||||
-webkit-text-stroke: 0.5px black;
|
||||
}
|
||||
|
||||
.textblock h1 {
|
||||
font-family: 'Roboto', sans-serif;
|
||||
font-size: 7vh;
|
||||
font-weight: bold;
|
||||
margin: 10px 0 0 0;
|
||||
}
|
||||
|
||||
.textblock p {
|
||||
font-size: x-large;
|
||||
font-style: italic;
|
||||
font-weight: 400;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.grid {
|
||||
width: 100%;
|
||||
|
@ -115,7 +151,7 @@
|
|||
}
|
||||
}
|
||||
|
||||
/* @media (prefers-color-scheme: dark) {
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.card,
|
||||
.footer {
|
||||
border-color: #222;
|
||||
|
@ -126,4 +162,4 @@
|
|||
.logo img {
|
||||
filter: invert(1);
|
||||
}
|
||||
} */
|
||||
}
|
|
@ -0,0 +1,107 @@
|
|||
export const exercises = [];
|
||||
exercises.push({
|
||||
name: 'Push Ups',
|
||||
imgSrc:
|
||||
'https://images.pexels.com/photos/3837781/pexels-photo-3837781.jpeg?auto=compress&cs=tinysrgb&w=1260&h=750&dpr=1',
|
||||
muscleGroups: ['Biceps', 'Chest', 'Core'],
|
||||
id: '0',
|
||||
});
|
||||
exercises.push({
|
||||
name: 'Sit Ups',
|
||||
imgSrc:
|
||||
'https://images.pexels.com/photos/3837781/pexels-photo-3837781.jpeg?auto=compress&cs=tinysrgb&w=1260&h=750&dpr=1',
|
||||
muscleGroups: ['Core'],
|
||||
id: '1',
|
||||
});
|
||||
exercises.push({
|
||||
name: 'Pull Ups',
|
||||
imgSrc:
|
||||
'https://images.pexels.com/photos/3837781/pexels-photo-3837781.jpeg?auto=compress&cs=tinysrgb&w=1260&h=750&dpr=1',
|
||||
muscleGroups: ['Biceps', 'Shoulders'],
|
||||
id: '2',
|
||||
});
|
||||
exercises.push({
|
||||
name: 'Squats',
|
||||
imgSrc:
|
||||
'https://images.pexels.com/photos/3837781/pexels-photo-3837781.jpeg?auto=compress&cs=tinysrgb&w=1260&h=750&dpr=1',
|
||||
muscleGroups: ['Legs', 'Glutes'],
|
||||
id: '3',
|
||||
});
|
||||
|
||||
export const workouts = [];
|
||||
workouts.push({
|
||||
name: 'Workout 1',
|
||||
imgSrc:
|
||||
'https://images.pexels.com/photos/3837781/pexels-photo-3837781.jpeg?auto=compress&cs=tinysrgb&w=1260&h=750&dpr=1',
|
||||
exercises: [{ 0: [5, 10] }, { 1: [5, 10] }],
|
||||
muscleGroups: ['Biceps', 'Chest', 'Core'],
|
||||
id: '4',
|
||||
});
|
||||
workouts.push({
|
||||
name: 'Workout 2',
|
||||
imgSrc:
|
||||
'https://images.pexels.com/photos/3837781/pexels-photo-3837781.jpeg?auto=compress&cs=tinysrgb&w=1260&h=750&dpr=1',
|
||||
exercises: [{ 2: [5, 10] }, { 0: [5, 10] }, { 1: [5, 10] }],
|
||||
muscleGroups: ['Biceps', 'Shoulders', 'Chest', 'Core'],
|
||||
id: '5',
|
||||
});
|
||||
workouts.push({
|
||||
name: 'Workout 3',
|
||||
imgSrc:
|
||||
'https://images.pexels.com/photos/3837781/pexels-photo-3837781.jpeg?auto=compress&cs=tinysrgb&w=1260&h=750&dpr=1',
|
||||
exercises: [{ 2: [5, 10] }, { 3: [5, 10] }],
|
||||
muscleGroups: ['Biceps', 'Shoulders', 'Legs', 'Glutes'],
|
||||
id: '6',
|
||||
});
|
||||
workouts.push({
|
||||
name: 'Workout 4',
|
||||
imgSrc:
|
||||
'https://images.pexels.com/photos/3837781/pexels-photo-3837781.jpeg?auto=compress&cs=tinysrgb&w=1260&h=750&dpr=1',
|
||||
exercises: [{ 1: [5, 10] }, { 3: [5, 10] }],
|
||||
muscleGroups: ['Core', 'Legs', 'Glutes'],
|
||||
id: '7',
|
||||
});
|
||||
workouts.push({
|
||||
name: 'Push Workout',
|
||||
imgSrc:
|
||||
'https://images.pexels.com/photos/3837781/pexels-photo-3837781.jpeg?auto=compress&cs=tinysrgb&w=1260&h=750&dpr=1',
|
||||
exercises: [{ 0: [5, 10] }],
|
||||
muscleGroups: ['Chest', 'Shoulder', 'Triceps'],
|
||||
id: '8',
|
||||
});
|
||||
workouts.push({
|
||||
name: 'Pull Workout',
|
||||
imgSrc:
|
||||
'https://images.pexels.com/photos/3837781/pexels-photo-3837781.jpeg?auto=compress&cs=tinysrgb&w=1260&h=750&dpr=1',
|
||||
exercises: [{ 2: [5, 10] }],
|
||||
muscleGroups: ['Back', 'Biceps', 'Abs'],
|
||||
id: '9',
|
||||
});
|
||||
workouts.push({
|
||||
name: 'Legs Workout',
|
||||
imgSrc:
|
||||
'https://images.pexels.com/photos/3837781/pexels-photo-3837781.jpeg?auto=compress&cs=tinysrgb&w=1260&h=750&dpr=1',
|
||||
exercises: [{ 3: [5, 10] }],
|
||||
muscleGroups: ['Quadriceps', 'Hamstrings', 'Calves'],
|
||||
id: '10',
|
||||
});
|
||||
workouts.push({
|
||||
name: 'Upper Workout',
|
||||
imgSrc:
|
||||
'https://images.pexels.com/photos/3837781/pexels-photo-3837781.jpeg?auto=compress&cs=tinysrgb&w=1260&h=750&dpr=1',
|
||||
exercises: [{ 2: [5, 10] }],
|
||||
muscleGroups: ['Chest', 'Back', 'Shoulder', 'Triceps'],
|
||||
id: '11',
|
||||
});
|
||||
|
||||
const photo = '../public/profile-pic.jpg';
|
||||
|
||||
export const userAuth = {
|
||||
uid: '12',
|
||||
name: 'John Doe',
|
||||
email: 'jdoe@gmail.com',
|
||||
photoURL: photo,
|
||||
role: 0,
|
||||
favouriteWorkouts: ['5', '7'],
|
||||
favouriteExercises: ['0', '3'],
|
||||
};
|
Loading…
Reference in New Issue