用React和Firebase创建一个健身追踪器

在这篇文章中,我们将使用React和Firebase构建一个健身追踪器网络应用,这两种技术使我们能够高效率地开发网络应用。

这篇文章将使你能够自己用React和Firebase构建全栈应用。如果你知道React的基础知识,你应该可以胜任。否则,我建议先解决这些问题。

注意,你可以在这里找到完成的应用程序在这里可以找到这个项目的源代码

Sign In Page For Fitness Tracker Application

设置项目

让我们先用craco npm包覆盖一个新的Create React App设置。Tailwind需要craco包来覆盖默认的Create React App配置。

让我们也来设置路由。我们将给我们的路由一个额外的参数,叫做layout ,这样它们就可以用正确的布局来包装页面。

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

我们将在本文的后面添加认证和其他路由。在App.js ,我们将返回我们的路由。让我们继续讨论用户会话。

用React Context认证用户会话

我们想知道一个用户是否在我们应用程序的任何地方通过了认证,而不需要通过多个组件传递这个信息。为了达到这个目的,我们将使用React的上下文API。我们将用认证上下文来包装我们的整个应用程序,这样我们就可以从我们应用程序的任何地方访问当前认证的用户。

首先,让我们创建一个认证上下文。

我们可以通过调用创建一个新的上下文。

const AuthContext = createContext()
复制代码

然后,我们将它提供给其他组件,像这样。

<AuthContext.Provider value={user}>{children}</AuthContext.Provider>
复制代码

在我们的例子中,我们想订阅已认证的Firebase用户。我们通过调用我们导出的Firebase auth函数的onAuthStateChanged() 方法来做到这一点。

 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文件中导出的auth对象上。

我们可以不直接访问这个方法,而是把它添加到我们的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>
  );
复制代码

在我们的登录页面上,我们现在可以用我们的useAuth() 钩子轻松访问signIn() 方法。

const { signIn } = useAuth();
复制代码

如果用户成功登录,我们将把他们重定向到仪表板,它位于主路由器路径上。为了检查这个,我们将使用一个try-catch块。

你现在应该收到一条错误信息,说没有找到用户,因为我们还没有签到。如果是这样,那太好了!这意味着我们的Firebase连接是正常的。这意味着我们的Firebase连接正在工作。

启用谷歌认证

首先,在Firebase控制台中启用Google认证。然后,在认证上下文中添加signInWithGoogle 函数。

function signInWithGoogle() {
    return auth.signInWithPopup(googleProvider);
}
复制代码

接下来,我们将从我们的Firebase文件中导入googleProvider

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>
  );
};
复制代码

现在我们可以向我们的reducer调度动作了。这是一个来自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 钩子来管理状态。这样一来,我们就有了高效的状态管理,以便在用户每次请求时都能检索到最新的练习列表。

function useWorkoutDb() {
  const [workoutDbState, dispatch] = useReducer(reducer, initialState);

  const { user } = useAuth();

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

    return database.exercises
      .where("userId", "==", user.uid)
      .onSnapshot((snapshot) => {
        dispatch({
          type: ACTIONS.SET_EXERCISES,
          payload: snapshot.docs.map(formatDocument),
        });
      });
  }, [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中创建锻炼方案

在我们的应用程序中,我们希望用户能够。

  • 添加和删除练习
  • 添加和删除组数
  • 对于每组,添加重量和次数
  • 对于每一组,标记为完成或未完成

Dashboard To Edit Workout In Fitness Tracker Application

我们可以通过向我们之前制作的减速器派发动作来更新我们的锻炼。为了更新重量,我们将派发以下动作。

dispatch({
      type: "UPDATE_WEIGHT",
      payload: {
        exerciseId,
        setId,
        newWeight,
      },
});
复制代码

然后,我们的还原器将相应地更新状态。

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

由于我们给了它exerciseIdsetId ,所以还原器知道要更新哪条记录。

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

创建仪表盘

仪表板由两个图表组成:总锻炼量和每天消耗的卡路里。我们还想显示今天、这周和这个月的锻炼总量和卡路里。

Dashboard Tracking Calories Burned Per Day On Graph

这意味着我们要从数据库中检索所有的锻炼,我们可以从我们的自定义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轴上是卡路里数。对线下的区域进行样式设计也很好,所以我们将使用rechartsAreaChart 组件。我们简单地传递给它一个数据数组。

 <AreaChart data={data}>
复制代码

接下来让我们对数据数组进行格式化。为了让recharts知道它需要使用x轴的月份,我们将在我们的AreaChart 内添加<XAxis dataKey="month" />

为了使其发挥作用,我们需要使用这种格式。

 [{ 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/…

The postCreate a fitness tracker with React and Firebaseappeared first onLogRocket Blog.

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享