はじめに
こちらの記事は、@Sicut_studyさんがアップしている【Reactアプリ100本ノック】シリーズに相乗りし、アウトプットを行うための記事になります。
- 実装ルールや成果物の達成条件は元記事に従うものとします。
- 元記事との差別点として、具体的に自分がどんな実装を行ったのか(と必要に応じて解説)を記載します。
@Sicut_studyさんのノック100本についていき、Reactを100日間学ぶのが目標です。
今回の元記事はこちら
前回の記事
問題
タイマーアプリを作成する
ルール
元記事より引用
- 主要なライブラリやフレームワークはReactである必要がありますが、その他のツールやライブラリ(例: Redux, Next.js, Styled Componentsなど)を組み合わせて使用することは自由
- TypeScriptを利用する
- 要件をみたせばデザインなどは自由
達成条件
元記事より引用
- ユーザーは分と秒を入力してタイマーを設定できる。
- スタートボタンを押すとカウントダウンが開始される。
- 時間が0になったら、ユーザーに通知される (効果音が鳴る)
- 一時停止ボタンを押すとカウントダウンが停止し、再開ボタンでカウントダウンが再開される。
- リセットボタンでタイマーが設定時間に戻る。
- 無効な時間(例:負の時間、非数値、60分以上の値)が入力された場合、エラーメッセージが表示される。
実装
本記事では以下の方針で実装していきます。
- ボタンとテキストボックスをコンポーネント化する
-
use-sound
ライブラリを利用して効果音を鳴らす
毎回同じコマンドを打つだけなので、今回からはプロジェクトの作成手順は省略します。
また、Emotionはstyled
ではなくcss
の方を使っていこうと思います(個人の好みから)。
各コンポーネントとApp.tsxの実装は以下のようになりました。効果音のmp3ファイルは好きなものを配置してください(※mp3のimportでエラーが出る場合)。
/** @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をimportするとき、以下のようなエラーが出るかもしれません。
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;
}
完成
完成形はこのようになりました。
最後に
ハイペース投稿でようやく本家のReactアプリ100本ノックに追いつきました。引き続き、100回完走を目指して継続していきます。
応援してくれる方はぜひフォローいただけると嬉しいです。
いいね、ストックもお待ちしております。
ではまた。
次回の記事