实践 React 应用百题挑战之 〜 04 计时器 〜

首先

这篇文章是为了参与@Sicut_study所发布的【React应用100题】系列,并进行输出而撰写的。

    • 実装ルールや成果物の達成条件は元記事に従うものとします。

 

    元記事との差別点として、具体的に自分がどんな実装を行ったのか(と必要に応じて解説)を記載します。

跟随Sicut_study先生进行100个挑战,目标是学习React 100天。

这篇原文可以在这里找到。

 

上一篇文章

 

请问这个问题。

创建一个计时器应用

– 规则

从原始文章中引用。

    • 主要なライブラリやフレームワークはReactである必要がありますが、その他のツールやライブラリ(例: Redux, Next.js, Styled Componentsなど)を組み合わせて使用することは自由

 

    • TypeScriptを利用する

 

    要件をみたせばデザインなどは自由

实现要求

从原始文章中引用

    1. 用户可以输入分钟和秒钟来设置定时器。

 

    1. 按下开始按钮后,倒计时将开始。

 

    1. 当时间到达0时,用户将收到通知(播放音效)。

 

    1. 按下暂停按钮将暂停倒计时,再次按下恢复按钮将重新开始倒计时。

 

    1. 按下重置按钮将定时器重设为设定的时间。

 

    如果输入了无效的时间(例如:负数时间、非数字、超过60分钟的值),将显示错误消息。

实施

本文将按照以下策略进行实施。

    • ボタンとテキストボックスをコンポーネント化する

use-soundライブラリを利用して効果音を鳴らす

由于每次只需要输入相同的命令,所以从这次开始,我们将省略项目的创建步骤。
此外,我打算在Emotion中使用css而不是styled(基于个人喜好)。

以下是每个组件和App.tsx的实现。请将喜欢的mp3文件放置在适当位置(如果导入mp3时出现错误,请注意)。

/** @jsxImportSource @emotion/react */
import { css } from "@emotion/react";

interface ButtonProps {
  onClick: () => void;
  disabled: boolean;
  text: string;
}

const buttonStyle = (disabled: boolean) => css`
  margin: 10px;
  padding: 5px 10px;
  background-color: ${disabled ? "#ccc" : "#007bff"};
  color: ${disabled ? "#666" : "white"};
  border: none;
  border-radius: 4px;
  cursor: ${disabled ? "not-allowed" : "pointer"};
  &:hover {
    background-color: ${disabled ? "#ccc" : "#0056b3"};
  }
`;

const Button = ({ onClick, disabled, text }: ButtonProps) => {
  return (
    <button css={buttonStyle(disabled)} onClick={onClick} disabled={disabled}>
      {text}
    </button>
  );
};

export default Button;
/** @jsxImportSource @emotion/react */
import { css } from "@emotion/react";

interface InputFieldProps {
  label: string;
  value: string;
  onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
}

const inputFieldStyle = css`
  margin: 10px;
  label {
    font-weight: bold;
  }
  input {
    margin-left: 5px;
    padding: 5px;
    border: 1px solid #ccc;
    border-radius: 4px;
  }
`;

const InputField = ({ label, value, onChange }: InputFieldProps) => {
  return (
    <div css={inputFieldStyle}>
      <label>
        {label}
        <input type="number" value={value} onChange={onChange} />
      </label>
    </div>
  );
};

export default InputField;
/** @jsxImportSource @emotion/react */
import { useState, useEffect } from "react";
import { css } from "@emotion/react";
import InputField from "./components/InputField";
import Button from "./components/Button";
import useSound from "use-sound";
import beepSound from "./sounds/beep.mp3";

const appStyle = css`
  text-align: center;
  margin-top: 50px;
`;

const errorStyle = css`
  color: red;
  margin-top: 20px;
`;

const App = () => {
  const [minutes, setMinutes] = useState<string>("0");
  const [seconds, setSeconds] = useState<string>("0");
  const [totalSeconds, setTotalSeconds] = useState<number>(0);
  const [isActive, setIsActive] = useState<boolean>(false);
  const [intervalId, setIntervalId] = useState<NodeJS.Timeout | null>(null);
  const [errorMessage, setErrorMessage] = useState<string | null>(null);
  // useSoundフックでサウンドをセットアップ
  const [play] = useSound(beepSound, { volume: 0.5 });

  useEffect(() => {
    let id: NodeJS.Timeout | null = null;
    if (isActive && totalSeconds > 0) {
      id = setInterval(() => {
        setTotalSeconds((seconds) => seconds - 1);
      }, 1000);
      setIntervalId(id);
    } else if (totalSeconds === 0 && isActive) {
      clearInterval(intervalId as NodeJS.Timeout);
      play(); // 効果音を再生する
      setIsActive(false);
    }
    return () => {
      if (id) clearInterval(id);
    };
  }, [isActive, totalSeconds]);

  const toggleActive = () => {
    setIsActive(!isActive);
  };

  const handleStart = () => {
    const totalSec = parseInt(minutes) * 60 + parseInt(seconds);
    if (!isNaN(totalSec) && totalSec >= 0) {
      setTotalSeconds(totalSec);
      setIsActive(true);
      setErrorMessage(null);
    } else {
      setErrorMessage("Invalid time input!");
    }
  };

  const handleReset = () => {
    setIsActive(false);
    setTotalSeconds(parseInt(minutes) * 60 + parseInt(seconds));
  };

  const handleMinutesChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    setMinutes(e.target.value);
  };

  const handleSecondsChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    setSeconds(e.target.value);
  };

  return (
    <div css={appStyle}>
      <h1>Timer App</h1>
      {errorMessage && <div css={errorStyle}>{errorMessage}</div>}
      <InputField
        label="Minutes:"
        value={minutes}
        onChange={handleMinutesChange}
      />
      <InputField
        label="Seconds:"
        value={seconds}
        onChange={handleSecondsChange}
      />
      <Button onClick={handleStart} disabled={isActive} text="Start" />
      <Button
        onClick={toggleActive}
        disabled={!isActive && totalSeconds === 0}
        text={isActive ? "Pause" : "Resume"}
      />
      <Button
        onClick={handleReset}
        disabled={!isActive && totalSeconds === 0}
        text="Reset"
      />
      <h2>
        Time Remaining: {Math.floor(totalSeconds / 60)}:
        {totalSeconds % 60 < 10 ? `0${totalSeconds % 60}` : totalSeconds % 60}
      </h2>
    </div>
  );
};

export default App;

提供完善

导入mp3时,可能会出现以下错误。

Cannot find module './sounds/beep.mp3' or its corresponding type declarations.

当出现这样的错误时,您可以创建src/types/custom.d.ts文件,并编写以下代码来解决问题。

declare module "*.mp3" {
  const src: string;
  export default src;
}

做完

完成的形式是这样的。

image.png
image.png
image.png

最后

我终于赶上了原版的React应用程序100个系列的快速帖子的节奏。我将继续努力并力争完赛100次。
如果你支持我,希望能够关注我,我会很高兴。
喜欢和收藏也期待着。

再见。

下一篇文章

 

广告
将在 10 秒后关闭
bannerAds