DevGang
Авторизоваться

Создайте фитнес-трекер с React и Firebase 

В этой статье мы создадим веб-приложение для фитнес-трекера, используя React и Firebase - две технологии, которые позволяют нам разрабатывать веб-приложения с высокой эффективностью.

Эта статья позволит вам самостоятельно создавать полнофункциональные приложения с React и Firebase. Если вы знаете основы React, все готово. В противном случае я бы посоветовал в первую очередь заняться ими.

Обратите внимание, что здесь вы можете найти готовое приложение, а здесь - исходный код этого проекта.

Настройка проекта

Начнем с перезаписи новой установки Create React App с помощью пакета craco npmTailwind нужен пакет craco, чтобы перезаписать конфигурацию приложения Create React по умолчанию.

Давайте также настроим маршрутизацию. Мы дадим нашим маршрутам дополнительный параметр под названием layout, чтобы они могли обернуть страницу с правильным макетом:

function RouteWrapper({ page: Page, layout: Layout, ...rest }) {
  return (
    <Route
      {...rest}
      render={(props) => (
        <Layout {...props}>
          <Page {...props} />
        </Layout>
      )}
    />
  );
}

Позже в этой статье мы добавим аутентификацию и другие маршруты. Внутри App.js вернем наш роутер. Продолжим пользовательские сеансы.

Аутентификация пользовательских сессий с помощью React Context

Мы хотели бы знать, аутентифицируется ли пользователь в любом месте нашего приложения без необходимости передавать эту информацию через несколько компонентов. Для этого мы будем использовать контекстный API React. Мы обернем все наше приложение контекстом аутентификации, чтобы мы могли получить доступ к текущему аутентифицированному пользователю из любой точки нашего приложения.

Во-первых, давайте создадим контекст аутентификации.

Мы можем создать новый контекст, вызвав:

const AuthContext = createContext()

Затем мы предоставим его другим компонентам, например:

<AuthContext.Provider value={user}>{children}</AuthContext.Provider>

В нашем случае мы хотим подписаться на аутентифицированного пользователя Firebase. Мы делаем это, вызывая метод onAuthStateChanged() нашей экспортированной функции аутентификации Firebase:

 auth.onAuthStateChanged(user => { … });

Это даст нам текущего аутентифицированного пользователя. Если состояние пользователя изменяется, например, при входе в систему или выходе из системы, мы хотим соответствующим образом обновить наш поставщик контекста. Чтобы обработать это изменение, мы будем использовать хук useEffect.

Наш AuthContext.jsx тогда выглядит так:

...
export function AuthProvider({ children }) {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    const unsubscribe = auth.onAuthStateChanged((user) => {
      setUser(user);
      setLoading(false);
    });

    return unsubscribe;
  }, []);

  const value = {
    user,
  };

  return (
    <AuthContext.Provider value={value}>
      {!loading && children}
    </AuthContext.Provider>
  );
}

Мы можем вызвать хук useAuth(), чтобы вернуть текущее значение контекста из любого места в нашем приложении. Значение контекста, которое мы предоставляем сейчас, позже будет содержать больше функций, таких как вход и выход.

Создание форм входа и регистрации

Чтобы иметь возможность войти в систему, нам нужно использовать метод с именем signInWithEmailAndPassword(), который находится в объекте аутентификации, который мы экспортируем в наш файл Firebase.

Вместо прямого доступа к этому методу мы можем добавить его к нашему провайдеру AuthContext, чтобы мы могли легко комбинировать методы аутентификации с аутентифицированным пользователем. Мы добавим функцию signIn() к нашему провайдеру AuthContext, например:

function signIn(email, password) {
    return auth.signInWithEmailAndPassword(email, password);
  }

  const value = {
    user,
    signIn,
  };

  return (
    <AuthContext.Provider value={value}>
      {!loading && children}
    </AuthContext.Provider>
  );

На нашей странице входа мы теперь можем легко получить доступ к методу signIn() с помощью нашего хука useAuth():

const { signIn } = useAuth();

Если пользователь успешно войдет в систему, мы перенаправим его на панель управления, которая находится на пути к домашнему маршрутизатору. Чтобы проверить это, мы воспользуемся блоком try-catch.

Теперь вы должны получить сообщение об ошибке, в котором говорится, что пользователь не найден, поскольку мы еще не зарегистрированы. Если да, то отлично! Это означает, что наше соединение с Firebase работает.

Включение аутентификации Google

Сначала включите аутентификацию Google в консоли Firebase. Затем добавьте функцию signInWithGoogle в контекст аутентификации:

function signInWithGoogle() {
    return auth.signInWithPopup(googleProvider);
}

Затем мы импортируем googleProvider из нашего файла Firebase:

export const googleProvider = new firebase.auth.GoogleAuthProvider();

Вернувшись на нашу страницу входа, мы добавим следующий код, чтобы это работало:

const handleGoogleSignIn = async () => {
    try {
      setGoogleLoading(true);
      await signInWithGoogle();
      history.push("/");
    } catch (error) {
      setError(error.message);
    }

    setGoogleLoading(false);
  };

Давайте продолжим создание нашего приложения для тренировок.

Создание параметров тренировки в приложении для фитнеса

Давайте создадим реальный компонент с именем SelectExercise. Внутри этого компонента мы хотим выполнить две вещи. Во-первых, мы хотим отобразить список упражнений, созданных пользователем, которые они могут добавить в свою тренировку. Во-вторых, мы хотим дать пользователю возможность создать новое упражнение.

Редуктор тренировки объединяет все наше приложение с состоянием тренировки, чтобы мы могли получить к нему доступ из любого места в нашем приложении. Каждый раз, когда пользователь меняет свою тренировку, localStorage обновляется, как и все компоненты, которые подписаны на состояние.

Мы отделяем тренировку от поставщика диспетчеризации, потому что некоторым компонентам нужен только доступ к состоянию или диспетчеризации:

const WorkoutStateContext = createContext();
const WorkoutDispatchContext = createContext();

export const useWorkoutState = () => useContext(WorkoutStateContext);
export const useWorkoutDispatch = () => useContext(WorkoutDispatchContext);

export const WorkoutProvider = ({ children }) => {
  const [workoutState, dispatch] = useReducer(rootReducer, initializer);

  // Persist workout state on workout update
  useEffect(() => {
    localStorage.setItem("workout", JSON.stringify(workoutState));
  }, [workoutState]);

  return (
    <WorkoutStateContext.Provider value={workoutState}>
      <WorkoutDispatchContext.Provider value={dispatch}>
        {children}
      </WorkoutDispatchContext.Provider>
    </WorkoutStateContext.Provider>
  );
};

Теперь мы можем отправлять действия нашему редуктору. Это концепция Redux - это просто означает, что мы хотим, чтобы состояние тренировки изменилось с указанным нами значением.

Наша функция addExercise выглядит так:

const exercise = {
      exerciseName,
      sets: { [uuidv4()]: DEFAULT_SET },
    };

    dispatch({
      type: "ADD_EXERCISE",
      payload: { exerciseId: uuidv4(), exercise },
    });

Она отправляет действие ADD_EXERCISE нашему редуктору, который добавит данное упражнение в наше состояние. При использовании Immer наш редуктор будет выглядеть так:

export const rootReducer = produce((draft, { type, payload }) => {
  switch (type) {
    ...
    case ACTIONS.ADD_EXERCISE:
      draft.exercises[payload.exerciseId] = payload.exercise;
      break;
    …

В наших упражнениях вместо массива объектов мы будем использовать объекты объектов.

case ACTIONS.UPDATE_WEIGHT:
      draft.exercises[payload.exerciseId].sets[payload.setId].weight =
        payload.weight;
      break;

Это намного эффективнее, чем фильтрация массива каждый раз, когда мы обновляем состояние, потому что редуктор точно знает, какой элемент обновлять.

SelectExercise также должна иметь возможность добавлять упражнения в базу данных. Итак, сначала нам нужен доступ к нашей базе данных Firestore.

Вот функция для сохранения нового упражнения в базе данных:

const { user } = useAuth();
...
const saveExercise = async () => {
    if (!exerciseName) {
      return setError("Please fill in all fields");
    }

    setError("");

    try {
      await database.exercises.add({
        exerciseName,
        userId: user.uid,
        createdAt: database.getCurrentTimestamp(),
      });

      toggleShowCreateExercise();
    } catch (err) {
      setError(err.message);
    }
};

Мы также захотим получить список упражнений, которые хранятся в базе данных, созданной пользователем. Мы не хотим оборачивать все наше приложение этими упражнениями, поэтому мы сохраним их локально в нашем компоненте SelectExercise.

Для получения упражнений с базой данных нам не нужен контекстный API. Ради обучения мы создадим настраиваемый хук, которая будет использовать хук useReducer для управления состоянием. Таким образом, у нас есть эффективное управление состоянием для получения обновленного списка упражнений каждый раз, когда пользователь запрашивает их.

}, [user]);

  useEffect(() => {
    dispatch({ type: ACTIONS.FETCHING_WORKOUTS });

    return database.workouts
      .where("userId", "==", user.uid)
      .onSnapshot((snapshot) => {
        dispatch({
          type: ACTIONS.SET_WORKOUTS,
          payload: snapshot.docs.map(formatDocument),
        });
      });
  }, [user]);

  return workoutDbState;
}

Вы можете заметить разницу между нашим другим useReducer, где мы используем объекты объектов и Immer для изменения состояния.

Теперь вы можете добавить упражнения и увидеть их в списке. Потрясающие! Давайте продолжим с таймером тренировки.

Создание таймера тренировки

Для таймера мы создадим специальный хук под названием useTimer. Мы установим интервал каждую секунду для обновления числовой переменной secondsPassed. Наши функции остановки и паузы очищают интервал, чтобы снова начать с 0. Каждую секунду мы также будем обновлять время внутри пользователя localStorage, чтобы пользователь мог обновить экран и при этом таймер по-прежнему работал правильно.

function useTimer() {
  const countRef = useRef();
  const [isActive, setIsActive] = useState(false);
  const [isPaused, setIsPaused] = useState(false);
  const [secondsPassed, setSecondsPassed] = useState(
    persist("get", "timer") || 0
  );

  useEffect(() => {
    const persistedSeconds = persist("get", "timer");
    if (persistedSeconds > 0) {
      startTimer();
      setSecondsPassed(persistedSeconds);
    }
  }, []);

  useEffect(() => {
    persist("set", "timer", secondsPassed);
  }, [secondsPassed]);

  const startTimer = () => {
    setIsActive(true);
    countRef.current = setInterval(() => {
      setSecondsPassed((seconds) => seconds + 1);
    }, 1000);
  };

  const stopTimer = () => {
    setIsActive(false);
    setIsPaused(false);
    setSecondsPassed(0);
    clearInterval(countRef.current);
  };

  const pauseTimer = () => {
    setIsPaused(true);
    clearInterval(countRef.current);
  };

  const resumeTimer = () => {
    setIsPaused(false);
    startTimer();
  };

  return {
    secondsPassed,
    isActive,
    isPaused,
    startTimer,
    stopTimer,
    pauseTimer,
    resumeTimer,
  };
}

Теперь таймер должен работать. Продолжим собственно схему тренировки.

Создание схемы тренировки в React

В нашем приложении мы хотим, чтобы пользователь мог:

  1. Добавить и удалить упражнения
  2. Добавить и удалить сеты
  3. Для каждого подхода добавить вес и количество повторений.
  4. Для каждого сета отметить как законченный или незаконченный

Мы можем обновить нашу тренировку, отправив действия в редуктор, который мы сделали ранее. Чтобы обновить вес, мы отправим следующее действие:

dispatch({
      type: "UPDATE_WEIGHT",
      payload: {
        exerciseId,
        setId,
        newWeight,
      },
});

Затем наш редуктор обновит состояние соответственно:

case ACTIONS.UPDATE_WEIGHT:
      draft.exercises[payload.exerciseId].sets[payload.setId].weight =
        payload.weight;

Редуктор знает, какую запись обновлять, поскольку мы даем ему exerciseId и setId:

<Button
                      icon="check"
                      variant={isFinished ? "primary" : "secondary"}
                      action={() =>
                        dispatch({
                          type: "TOGGLE_FINISHED",
                          payload: {
                            exerciseId,
                            setId,
                          },
                        })
                      }
/>

Создание панели управлений

Панель управления состоит из двух графиков: общее количество тренировок и количество сожженных калорий за день. Мы также хотим отображать общее количество тренировок и калорий за сегодня, на этой неделе и в этом месяце.

Это означает, что мы хотим получить все тренировки из базы данных, которую мы можем получить с помощью нашего настраиваемого хука useWorkoutDb():

const { isFetchingWorkouts, workouts } = useWorkoutDb();

Мы уже можем отображать общее количество тренировок:

{isFetchingWorkouts ? 0: workouts.length}

Сожженные калории за день, неделю и месяц

Если тренировка изменилась и в ней есть хотя бы одно упражнение, мы хотим пересчитать калории:

useEffect(() => {
  if (!isFetchingWorkouts && workouts.length) {
    calcCalories();
  }
}, [workouts])

Для каждой тренировки мы проверяем, совпадает ли дата с сегодняшним днем, на этой неделе или в этом месяце.

const formattedDate = new Date(createdAt.seconds * 1000);
const day = format(formattedDate, "d");

Если это так, мы соответствующим образом обновим калории, умножив их на количество минут, прошедших на этой тренировке:

const newCalories = CALORIES_PER_HOUR * (secondsPassed / 3600);

    if (dayOfYear === day) {
      setCalories((calories) => ({
        ...calories,
        today: calories.today + newCalories,
      }));
    }
} 

График тренировки

Нам нужна простая линейная диаграмма с месяцами на оси x и количеством калорий на оси y. Также неплохо стилизовать область под линией, поэтому мы будем использовать компонент recharts AreaChart. Мы просто передаем ему массив данных:

 <AreaChart data={data}>

Теперь отформатируем массив данных. Чтобы дать знать, что он должен использовать месяц для оси x, мы добавим <XAxis dataKey="month" /> внутри нашего AreaChart.

Чтобы это работало, нам нужно использовать такой формат:

 [{ month: "Feb", amount: 13 }, ...]

Мы хотим показать количество тренировок за последние три месяца, даже если в эти месяцы не было тренировок. Итак, давайте заполним массив за последние три месяца с помощью date-fns и установим сумму в 0.

const [data, setData] = useState([]);
let lastMonths = [];

    const addEmptyMonths = () => {
      const today = new Date();

      for (let i = 2; i >= 0; i--) {
        const month = format(subMonths(today, i), "LLL");
        lastMonths.push(month);
        setData((data) => [...data, { month, amount: 0 }]);
      }
    };

Создание таблицы калорий

Для диаграммы калорий мы хотим показать количество калорий в день за последнюю неделю. Подобно WorkoutChart, мы заполняем наши данные массивом дней последней недели с 0 калориями в день.

let lastDays = [];

    const addEmptyDays = () => {
      const today = new Date();

      for (let i = 6; i >= 0; i--) {
        const day = format(subDays(today, i), "E");
        lastDays.push(day);
        setData((data) => [...data, { day, calories: 0 }]);
      }
    };

Для каждой тренировки мы будем проверять, произошло ли это в течение последних семи дней. Если это так, мы рассчитаем количество калорий, сожженных за эту тренировку, и добавим его в наш массив данных:

const addCaloriesPerDay = () => {
      for (const { createdAt, secondsPassed } of workouts) {
        const day = format(new Date(createdAt.seconds * 1000), "E");
        const index = lastDays.indexOf(day);
        if (index !== -1) {
          const calories = CALORIES_PER_HOUR * (secondsPassed / 3600);

          setData((data) => {
            data[index].calories = data[index].calories + parseInt(calories);
            return data;
          });
        }
      }
    }; 

Если вы сохраните новую тренировку, вы должны увидеть, как обновляются статистические данные и графики на панели управления.

Поздравляем, вы создали фитнес-приложение React! Спасибо за то, что следуете этому руководству.

Готовое приложение вы можете найти здесь: fitlife-app.netlify.app. Исходный код можно найти здесь: github.com/sanderdebr/FitLife/

Источник:

#React
Комментарии
Чтобы оставить комментарий, необходимо авторизоваться

Присоединяйся в тусовку

Поделитесь своим опытом, расскажите о новом инструменте, библиотеке или фреймворке. Для этого не обязательно становится постоянным автором.

Попробовать

В подарок 100$ на счет при регистрации

Получить