forked from semi-23e/nextjs-todo-tutorial
Compare commits
18 Commits
implement-
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 253d25ab5c | |||
| f7d9950bbc | |||
| d387a00dff | |||
| f379dd5d8f | |||
| 1b92843a85 | |||
| 21b87f0756 | |||
| e29e4ff796 | |||
| 67cd70bbd6 | |||
| 33bf23aac4 | |||
| 62e2942cb9 | |||
| cd0e6cca9f | |||
| f51fbe074a | |||
| 437dcaf27c | |||
| 9ff988db7a | |||
| 97d0606a16 | |||
| c017844b9d | |||
| 72fdc8bdbe | |||
| 791411e209 |
270
README.md
270
README.md
@@ -1,36 +1,262 @@
|
|||||||
This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
|
# 🎯 TODOアプリで学ぶ Next.js & Git/Gitea 実践開発
|
||||||
|
|
||||||
## Getting Started
|
## 📚 このプロジェクトについて
|
||||||
|
|
||||||
First, run the development server:
|
このリポジトリは、プログラミング初学者の皆さんが**実践的なWebアプリケーション開発スキル**を身につけるための学習教材です。TODOアプリという身近な題材を通じて、最新のWeb開発技術とチーム開発の基礎を学びます。
|
||||||
|
|
||||||
```bash
|
### 🚀 学べる技術スキル
|
||||||
npm run dev
|
|
||||||
# or
|
#### 1. **モダンなWeb開発技術**
|
||||||
yarn dev
|
|
||||||
# or
|
- **Next.js 15** - 最新のReactフレームワーク
|
||||||
pnpm dev
|
- **TypeScript** - 型安全なJavaScript
|
||||||
# or
|
- **Tailwind CSS v4** - 効率的なスタイリング
|
||||||
bun dev
|
- **React 19** - 最新のUIライブラリ
|
||||||
|
|
||||||
|
#### 2. **実践的な開発スキル**
|
||||||
|
|
||||||
|
- バージョン管理(Git)
|
||||||
|
- チーム開発(Gitea)
|
||||||
|
- コードレビュー
|
||||||
|
- 問題解決能力
|
||||||
|
|
||||||
|
### 📊 学習時間の目安
|
||||||
|
|
||||||
|
- **TODOアプリ開発**: 15-20時間
|
||||||
|
- **Git/Gitea学習**: 18-25時間
|
||||||
|
- **合計**: 約30-45時間(1日2-3時間×2-3週間)
|
||||||
|
|
||||||
|
## 🗺️ 学習の進め方
|
||||||
|
|
||||||
|
### 推奨学習ルート
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph LR
|
||||||
|
A[環境構築] --> B[Git基礎]
|
||||||
|
B --> C[TODOアプリ Phase1-2]
|
||||||
|
C --> D[Git実践]
|
||||||
|
D --> E[TODOアプリ Phase3-5]
|
||||||
|
E --> F[Gitea連携]
|
||||||
|
F --> G[チーム開発体験]
|
||||||
```
|
```
|
||||||
|
|
||||||
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
|
### Step 1: 環境を整える(2-3時間)
|
||||||
|
|
||||||
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
|
まずは開発環境を準備しましょう:
|
||||||
|
|
||||||
This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
|
1. **WSL2のセットアップ**(Windows環境の場合)
|
||||||
|
- Ubuntuをインストール
|
||||||
|
- ターミナルの基本操作を覚える
|
||||||
|
|
||||||
## Learn More
|
2. **VSCodeのインストール**
|
||||||
|
- WSL拡張機能を追加
|
||||||
|
- 推奨拡張機能をインストール
|
||||||
|
|
||||||
To learn more about Next.js, take a look at the following resources:
|
3. **プロジェクトの起動確認**
|
||||||
|
|
||||||
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
|
```bash
|
||||||
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
|
# 開発サーバーを起動
|
||||||
|
npm run dev
|
||||||
|
|
||||||
|
# ブラウザで http://localhost:3000 を開く
|
||||||
|
```
|
||||||
|
|
||||||
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
|
### Step 2: 2つのカリキュラムを並行して進める
|
||||||
|
|
||||||
## Deploy on Vercel
|
#### 📱 TODOアプリ開発カリキュラム
|
||||||
|
|
||||||
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
|
**Phase 1: 基礎理解(2-3時間)**
|
||||||
|
|
||||||
Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.
|
- Next.jsプロジェクトの構造を理解
|
||||||
|
- 最初のコンポーネントを作成
|
||||||
|
- TypeScriptの基本を学ぶ
|
||||||
|
|
||||||
|
**Phase 2: UIの作成(3-4時間)**
|
||||||
|
|
||||||
|
- TODOリストの見た目を作る
|
||||||
|
- 入力フォームを追加
|
||||||
|
- Tailwind CSSでデザイン
|
||||||
|
|
||||||
|
**Phase 3: 機能実装(4-5時間)**
|
||||||
|
|
||||||
|
- ✅ TODO追加機能
|
||||||
|
- ✅ 完了/未完了の切り替え
|
||||||
|
- ✅ TODO削除機能
|
||||||
|
|
||||||
|
**Phase 4: 高度な機能(3-4時間)**
|
||||||
|
|
||||||
|
- ✏️ 編集機能
|
||||||
|
- 🔍 フィルター機能
|
||||||
|
- 💾 データの保存
|
||||||
|
|
||||||
|
**Phase 5: 仕上げ(2-3時間)**
|
||||||
|
|
||||||
|
- コンポーネントの整理
|
||||||
|
- エラー処理
|
||||||
|
- パフォーマンス改善
|
||||||
|
|
||||||
|
#### 🔀 Git/Gitea学習カリキュラム
|
||||||
|
|
||||||
|
**Phase 1: Git基礎(3-4時間)**
|
||||||
|
|
||||||
|
- バージョン管理の概念
|
||||||
|
- 基本コマンド(add, commit, push)
|
||||||
|
- VSCodeでのGit操作
|
||||||
|
|
||||||
|
**Phase 2: 実践的な使い方(4-5時間)**
|
||||||
|
|
||||||
|
- ブランチの作成と切り替え
|
||||||
|
- コンフリクトの解決
|
||||||
|
- 履歴の確認方法
|
||||||
|
|
||||||
|
**Phase 3: Gitea入門(3-4時間)**
|
||||||
|
|
||||||
|
- リポジトリの作成
|
||||||
|
- SSH鍵の設定
|
||||||
|
- プッシュとプル
|
||||||
|
|
||||||
|
**Phase 4: チーム開発体験(4-5時間)**
|
||||||
|
|
||||||
|
- Issue管理
|
||||||
|
- プルリクエスト
|
||||||
|
- コードレビュー
|
||||||
|
|
||||||
|
## ⚠️ 初心者がつまずきやすいポイント
|
||||||
|
|
||||||
|
### 1. TypeScriptのエラー対処法
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// よくあるエラー例
|
||||||
|
const [todos, setTodos] = useState([]); // ❌ 型が不明
|
||||||
|
|
||||||
|
// 解決方法
|
||||||
|
type Todo = {
|
||||||
|
id: number;
|
||||||
|
text: string;
|
||||||
|
completed: boolean;
|
||||||
|
};
|
||||||
|
const [todos, setTodos] = useState<Todo[]>([]); // ✅ 型を明示
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. WSL環境の注意点
|
||||||
|
|
||||||
|
- **ファイルの場所**: 必ず`~/projects/`以下で作業(Windows側の`C:\`は避ける)
|
||||||
|
- **改行コード**: LFに統一(CRLFは使わない)
|
||||||
|
- **パスの書き方**: `/home/username/projects/` のようにLinux形式で
|
||||||
|
|
||||||
|
### 3. Gitでよくある失敗
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# コミット前に必ず状態確認
|
||||||
|
git status
|
||||||
|
|
||||||
|
# 間違えてコミットした場合
|
||||||
|
git reset --soft HEAD~1 # 直前のコミットを取り消し
|
||||||
|
```
|
||||||
|
|
||||||
|
## 💡 学習のコツ
|
||||||
|
|
||||||
|
### 1. エラーは成長のチャンス
|
||||||
|
|
||||||
|
- エラーメッセージをよく読む
|
||||||
|
- 検索して解決方法を探す
|
||||||
|
- 質問する前に自分で試す
|
||||||
|
|
||||||
|
### 2. こまめにコミット
|
||||||
|
|
||||||
|
- 小さな機能ごとに保存
|
||||||
|
- 分かりやすいメッセージを書く
|
||||||
|
- 失敗を恐れない(いつでも戻せる)
|
||||||
|
|
||||||
|
### 3. 実際に手を動かす
|
||||||
|
|
||||||
|
- コピペではなく自分で入力
|
||||||
|
- 動作を確認しながら進める
|
||||||
|
- カスタマイズに挑戦する
|
||||||
|
|
||||||
|
## 🎓 学習のゴール
|
||||||
|
|
||||||
|
このプロジェクトを完了すると、以下のスキルが身につきます:
|
||||||
|
|
||||||
|
### 技術スキル
|
||||||
|
|
||||||
|
- ✅ React/Next.jsでWebアプリが作れる
|
||||||
|
- ✅ TypeScriptで型安全なコードが書ける
|
||||||
|
- ✅ Gitでバージョン管理ができる
|
||||||
|
- ✅ チーム開発の基本がわかる
|
||||||
|
|
||||||
|
### ソフトスキル
|
||||||
|
|
||||||
|
- ✅ エラーを自力で解決できる
|
||||||
|
- ✅ ドキュメントを読んで理解できる
|
||||||
|
- ✅ 計画的に開発を進められる
|
||||||
|
- ✅ 他人のコードを読める
|
||||||
|
|
||||||
|
## 🚀 次のステップ
|
||||||
|
|
||||||
|
このプロジェクトを完了したら、以下に挑戦してみましょう:
|
||||||
|
|
||||||
|
1. **機能の拡張**
|
||||||
|
- カテゴリー機能
|
||||||
|
- 期限設定
|
||||||
|
- 優先度管理
|
||||||
|
|
||||||
|
2. **新しい技術の導入**
|
||||||
|
- データベース連携
|
||||||
|
- 認証機能
|
||||||
|
- API開発
|
||||||
|
|
||||||
|
3. **実践プロジェクト**
|
||||||
|
- オリジナルアプリの開発
|
||||||
|
- オープンソースへの貢献
|
||||||
|
- チーム開発への参加
|
||||||
|
|
||||||
|
## 📞 困ったときは
|
||||||
|
|
||||||
|
### よくある質問(FAQ)
|
||||||
|
|
||||||
|
**Q: npm run devでエラーが出る**
|
||||||
|
A: `node_modules`を削除して`npm install`を実行してください
|
||||||
|
|
||||||
|
**Q: TypeScriptの型エラーが解決できない**
|
||||||
|
A: 一時的に`any`型を使い、後で正しい型に修正しましょう
|
||||||
|
|
||||||
|
**Q: Gitでプッシュできない**
|
||||||
|
A: リモートの最新を取得(`git pull`)してから再度プッシュ
|
||||||
|
|
||||||
|
### サポート方法
|
||||||
|
|
||||||
|
1. **自己解決を試みる**(15分)
|
||||||
|
- エラーメッセージを読む
|
||||||
|
- 公式ドキュメントを確認
|
||||||
|
- Google/Stack Overflowで検索
|
||||||
|
|
||||||
|
2. **質問の準備**
|
||||||
|
- 何をしようとしたか
|
||||||
|
- どんなエラーが出たか
|
||||||
|
- 何を試したか
|
||||||
|
|
||||||
|
3. **質問する**
|
||||||
|
- 具体的に説明する
|
||||||
|
- コードやエラーを共有
|
||||||
|
- 解決後は共有する
|
||||||
|
|
||||||
|
## 📂 プロジェクト構成
|
||||||
|
|
||||||
|
```
|
||||||
|
nextjs-todo-tutorial/
|
||||||
|
├── src/app/ # アプリケーションコード
|
||||||
|
├── public/ # 静的ファイル
|
||||||
|
├── package.json # 依存関係
|
||||||
|
├── tsconfig.json # TypeScript設定
|
||||||
|
├── tailwind.config.ts # Tailwind設定
|
||||||
|
├── CLAUDE.md # AI支援用ドキュメント
|
||||||
|
├── todo-app-curriculum.md # TODOアプリカリキュラム詳細
|
||||||
|
└── git-gitea-curriculum.md # Git/Giteaカリキュラム詳細
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📝 最後に
|
||||||
|
|
||||||
|
プログラミング学習は山登りのようなものです。一歩一歩進めば、必ず頂上にたどり着けます。エラーや困難は成長のチャンス。楽しみながら、自分のペースで学習を進めてください。
|
||||||
|
|
||||||
|
**Happy Coding! 🎉**
|
||||||
|
|||||||
@@ -102,6 +102,35 @@ git init
|
|||||||
git status
|
git status
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**💡 コマンドの詳細説明:**
|
||||||
|
|
||||||
|
**git initとは?**
|
||||||
|
|
||||||
|
- 現在のディレクトリをGitリポジトリにします
|
||||||
|
- `.git`という隠しフォルダが作られ、そこに変更履歴が保存されます
|
||||||
|
- 一度だけ実行すればOK
|
||||||
|
|
||||||
|
**git statusとは?**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 実行例
|
||||||
|
$ git status
|
||||||
|
On branch main
|
||||||
|
|
||||||
|
No commits yet # まだコミットがない
|
||||||
|
|
||||||
|
Untracked files: # Gitが追跡していないファイル
|
||||||
|
(use "git add <file>..." to include in what will be committed)
|
||||||
|
README.md
|
||||||
|
index.html
|
||||||
|
```
|
||||||
|
|
||||||
|
**状態の読み方:**
|
||||||
|
|
||||||
|
- 赤色 = 変更されたがステージングされていない
|
||||||
|
- 緑色 = ステージングされた(コミット準備OK)
|
||||||
|
- Untracked = Gitがまだ追跡していない新しいファイル
|
||||||
|
|
||||||
VSCodeでの確認:
|
VSCodeでの確認:
|
||||||
|
|
||||||
- 左サイドバーの「ソース管理」アイコンをクリック
|
- 左サイドバーの「ソース管理」アイコンをクリック
|
||||||
@@ -122,6 +151,44 @@ git status
|
|||||||
git commit -m "初回コミット: プロジェクトのセットアップ"
|
git commit -m "初回コミット: プロジェクトのセットアップ"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**💡 コマンドの詳細説明:**
|
||||||
|
|
||||||
|
**git addとは?**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 全てのファイルをステージングエリアに追加
|
||||||
|
git add .
|
||||||
|
|
||||||
|
# 特定のファイルだけ追加
|
||||||
|
git add README.md
|
||||||
|
|
||||||
|
# 特定の種類のファイルを追加
|
||||||
|
git add *.js
|
||||||
|
```
|
||||||
|
|
||||||
|
**ステージングエリアとは?**
|
||||||
|
|
||||||
|
- コミット前の「準備エリア」のようなもの
|
||||||
|
- ここに追加したファイルだけがコミットされる
|
||||||
|
- 間違えてaddした場合は`git reset`で取り消せる
|
||||||
|
|
||||||
|
**git commitとは?**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 基本形
|
||||||
|
git commit -m "メッセージ"
|
||||||
|
|
||||||
|
# 詳細なメッセージを書きたい場合
|
||||||
|
git commit # エディタが開く
|
||||||
|
```
|
||||||
|
|
||||||
|
**良いコミットメッセージの例:**
|
||||||
|
|
||||||
|
- ✅ `feat: TODO追加機能を実装`
|
||||||
|
- ✅ `fix: 空のTODOを追加できるバグを修正`
|
||||||
|
- ❌ `更新` (何を更新したか不明)
|
||||||
|
- ❌ `aaa` (意味がわからない)
|
||||||
|
|
||||||
##### VSCode GUI方式
|
##### VSCode GUI方式
|
||||||
|
|
||||||
1. ソース管理パネルを開く(Ctrl+Shift+G)
|
1. ソース管理パネルを開く(Ctrl+Shift+G)
|
||||||
@@ -142,6 +209,37 @@ git log --oneline
|
|||||||
git log --graph --oneline --all
|
git log --graph --oneline --all
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**💡 コマンドの詳細説明:**
|
||||||
|
|
||||||
|
**git logの様々な表示方法:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 詳細表示(デフォルト)
|
||||||
|
$ git log
|
||||||
|
commit a1b2c3d4e5f6... # コミットID(SHA-1ハッシュ)
|
||||||
|
Author: Your Name <email@example.com>
|
||||||
|
Date: Mon Jan 15 10:30:00 2024 +0900
|
||||||
|
|
||||||
|
feat: TODO追加機能を実装
|
||||||
|
|
||||||
|
# 1行表示(簡潔)
|
||||||
|
$ git log --oneline
|
||||||
|
a1b2c3d feat: TODO追加機能を実装
|
||||||
|
9876543 fix: バグ修正
|
||||||
|
|
||||||
|
# グラフ表示(ブランチが見える)
|
||||||
|
$ git log --graph --oneline
|
||||||
|
* a1b2c3d (HEAD -> main) feat: TODO追加機能を実装
|
||||||
|
* 9876543 fix: バグ修正
|
||||||
|
```
|
||||||
|
|
||||||
|
**便利なオプション:**
|
||||||
|
|
||||||
|
- `-n 5` - 最新の5件だけ表示
|
||||||
|
- `--since="2 weeks ago"` - 2週間以内のコミット
|
||||||
|
- `--author="Your Name"` - 特定の人のコミット
|
||||||
|
- `--grep="fix"` - メッセージに"fix"を含む
|
||||||
|
|
||||||
VSCode:
|
VSCode:
|
||||||
|
|
||||||
- Git Graph拡張機能でビジュアル表示
|
- Git Graph拡張機能でビジュアル表示
|
||||||
@@ -169,6 +267,42 @@ git branch
|
|||||||
git checkout -b feature/add-todo-edit
|
git checkout -b feature/add-todo-edit
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**💡 コマンドの詳細説明:**
|
||||||
|
|
||||||
|
**ブランチとは?**
|
||||||
|
|
||||||
|
- 開発の「枝分かれ」を作る機能
|
||||||
|
- mainブランチを安全に保ちながら、新機能を開発できる
|
||||||
|
- 失敗しても元に戻せる
|
||||||
|
|
||||||
|
**git branchの表示:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ git branch
|
||||||
|
* main # *がついているのが現在のブランチ
|
||||||
|
feature/todo-list
|
||||||
|
feature/styling
|
||||||
|
```
|
||||||
|
|
||||||
|
**git checkoutの使い方:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 新しいブランチを作成して切り替え(一番よく使う)
|
||||||
|
git checkout -b feature/new-feature
|
||||||
|
|
||||||
|
# 既存のブランチに切り替え
|
||||||
|
git checkout main
|
||||||
|
|
||||||
|
# ブランチを作成だけ(切り替えない)
|
||||||
|
git branch feature/test
|
||||||
|
```
|
||||||
|
|
||||||
|
**ブランチ名のルール(例):**
|
||||||
|
|
||||||
|
- `feature/機能名` - 新機能開発
|
||||||
|
- `fix/バグ名` - バグ修正
|
||||||
|
- `refactor/内容` - コード整理
|
||||||
|
|
||||||
VSCode:
|
VSCode:
|
||||||
|
|
||||||
- ステータスバー左下のブランチ名をクリック
|
- ステータスバー左下のブランチ名をクリック
|
||||||
@@ -186,6 +320,44 @@ git checkout main
|
|||||||
git merge feature/add-todo-edit
|
git merge feature/add-todo-edit
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**💡 コマンドの詳細説明:**
|
||||||
|
|
||||||
|
**マージとは?**
|
||||||
|
|
||||||
|
- 別ブランチの変更を現在のブランチに統合すること
|
||||||
|
- 枝分かれした開発を一つにまとめる
|
||||||
|
|
||||||
|
**マージの流れ:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. 現在のブランチを確認
|
||||||
|
git branch
|
||||||
|
feature/add-todo-edit
|
||||||
|
* main
|
||||||
|
|
||||||
|
# 2. マージ実行
|
||||||
|
git merge feature/add-todo-edit
|
||||||
|
Updating a1b2c3d..e5f6g7h
|
||||||
|
Fast-forward # 早送りマージ(コンフリクトなし)
|
||||||
|
src/app/page.tsx | 20 ++++++++++++++++++++
|
||||||
|
1 file changed, 20 insertions(+)
|
||||||
|
|
||||||
|
# 3. マージ後の確認
|
||||||
|
git log --oneline --graph -5
|
||||||
|
```
|
||||||
|
|
||||||
|
**Fast-forwardマージとは?**
|
||||||
|
|
||||||
|
- コンフリクトがないシンプルなマージ
|
||||||
|
- mainブランチがただ「進む」だけ
|
||||||
|
|
||||||
|
**マージ後のブランチ削除:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 不要になったブランチを削除
|
||||||
|
git branch -d feature/add-todo-edit
|
||||||
|
```
|
||||||
|
|
||||||
VSCodeでのマージ:
|
VSCodeでのマージ:
|
||||||
|
|
||||||
1. Git Graphで視覚的にブランチを確認
|
1. Git Graphで視覚的にブランチを確認
|
||||||
@@ -356,11 +528,76 @@ git config --global alias.lg "log --graph --oneline --all"
|
|||||||
- **問題**: Windows側のパスとWSL側のパスの混同
|
- **問題**: Windows側のパスとWSL側のパスの混同
|
||||||
- **対策**: 常にWSL側で作業、`pwd`でパス確認
|
- **対策**: 常にWSL側で作業、`pwd`でパス確認
|
||||||
|
|
||||||
|
**💡 具体的な例と解決法:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# パスの確認方法
|
||||||
|
$ pwd
|
||||||
|
/mnt/c/Users/... # ❌ Windows側(遅い)
|
||||||
|
/home/username/... # ✅ WSL側(速い)
|
||||||
|
|
||||||
|
# ホームディレクトリに移動
|
||||||
|
cd ~
|
||||||
|
|
||||||
|
# Windows側からWSL側にファイルをコピー
|
||||||
|
cp -r /mnt/c/Users/yourname/projects/todo-app ~/projects/
|
||||||
|
```
|
||||||
|
|
||||||
|
**VSCodeでの確認方法:**
|
||||||
|
|
||||||
|
- 左下に`WSL: Ubuntu`と表示されているか確認
|
||||||
|
- ターミナルで`pwd`を実行してパスを確認
|
||||||
|
|
||||||
### 2. 改行コードの不一致
|
### 2. 改行コードの不一致
|
||||||
|
|
||||||
- **問題**: WindowsのCRLFとLinuxのLFの混在
|
- **問題**: WindowsのCRLFとLinuxのLFの混在
|
||||||
- **対策**: `.gitattributes`で統一、VSCodeの設定で`LF`に固定
|
- **対策**: `.gitattributes`で統一、VSCodeの設定で`LF`に固定
|
||||||
|
|
||||||
|
**💡 詳細な解決方法:**
|
||||||
|
|
||||||
|
**エラーの例:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ git add .
|
||||||
|
$ git status
|
||||||
|
warning: LF will be replaced by CRLF in src/app/page.tsx.
|
||||||
|
The file will have its original line endings in your working directory
|
||||||
|
```
|
||||||
|
|
||||||
|
**解決手順:**
|
||||||
|
|
||||||
|
1. **Gitの設定を変更**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# WSL側で実行
|
||||||
|
git config --global core.autocrlf input
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **`.gitattributes`ファイルを作成**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
echo "* text=auto eol=lf" > .gitattributes
|
||||||
|
git add .gitattributes
|
||||||
|
git commit -m "fix: 改行コードをLFに統一"
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **既存ファイルの改行コードを修正**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 全ファイルの改行コードをリセット
|
||||||
|
git rm --cached -r .
|
||||||
|
git reset --hard
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **VSCodeの設定を変更**
|
||||||
|
|
||||||
|
```json
|
||||||
|
// .vscode/settings.json
|
||||||
|
{
|
||||||
|
"files.eol": "\n" // LFに固定
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
### 3. VSCodeが重い
|
### 3. VSCodeが重い
|
||||||
|
|
||||||
- **問題**: Windows側のファイルをWSL経由で編集
|
- **問題**: Windows側のファイルをWSL経由で編集
|
||||||
@@ -371,6 +608,88 @@ git config --global alias.lg "log --graph --oneline --all"
|
|||||||
- **問題**: HTTPSでのpush時に認証失敗
|
- **問題**: HTTPSでのpush時に認証失敗
|
||||||
- **対策**: SSH接続の使用、または認証トークンの設定
|
- **対策**: SSH接続の使用、または認証トークンの設定
|
||||||
|
|
||||||
|
**💡 詳細な解決方法:**
|
||||||
|
|
||||||
|
**エラーの例:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ git push origin main
|
||||||
|
Username for 'https://gitea.example.com': yourname
|
||||||
|
Password for 'https://yourname@gitea.example.com':
|
||||||
|
remote: Invalid username or password.
|
||||||
|
fatal: Authentication failed
|
||||||
|
```
|
||||||
|
|
||||||
|
**解決方法1:SSHに切り替える(推奨)**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. 現在のリモートURLを確認
|
||||||
|
git remote -v
|
||||||
|
|
||||||
|
# 2. HTTPSからSSHに変更
|
||||||
|
git remote set-url origin git@gitea.example.com:username/todo-app.git
|
||||||
|
|
||||||
|
# 3. SSH鍵が設定されているか確認
|
||||||
|
ssh -T git@gitea.example.com
|
||||||
|
```
|
||||||
|
|
||||||
|
**解決方法2:アクセストークンを使う**
|
||||||
|
|
||||||
|
1. Giteaでアクセストークンを生成
|
||||||
|
- 設定 → アプリケーション → アクセストークンを生成
|
||||||
|
|
||||||
|
2. 認証情報を保存
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 認証情報ヘルパーを設定
|
||||||
|
git config --global credential.helper store
|
||||||
|
|
||||||
|
# 次回のpush時にユーザー名とトークンを入力
|
||||||
|
# パスワードの代わりにアクセストークンを使用
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. ファイルが表示されない/消えた
|
||||||
|
|
||||||
|
**💡 よくあるシナリオと解決法:**
|
||||||
|
|
||||||
|
**ケース1:.gitignoreに追加されている**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# .gitignoreの内容を確認
|
||||||
|
cat .gitignore
|
||||||
|
|
||||||
|
# 特定のファイルがgitに追跡されているか確認
|
||||||
|
git ls-files | grep "filename"
|
||||||
|
|
||||||
|
# .gitignoreされているファイルを強制的に追加
|
||||||
|
git add -f filename
|
||||||
|
```
|
||||||
|
|
||||||
|
**ケース2:間違えて削除した**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 削除したファイルを復元
|
||||||
|
git checkout HEAD -- filename
|
||||||
|
|
||||||
|
# または、特定のコミットから復元
|
||||||
|
git checkout abc123 -- filename
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6. コミットメッセージを間違えた
|
||||||
|
|
||||||
|
**💡 修正方法:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 直前のコミットメッセージを修正
|
||||||
|
git commit --amend -m "新しいメッセージ"
|
||||||
|
|
||||||
|
# エディタで編集
|
||||||
|
git commit --amend
|
||||||
|
|
||||||
|
# 注意:すでにpushした場合は強制pushが必要(推奨しない)
|
||||||
|
# git push --force-with-lease origin main
|
||||||
|
```
|
||||||
|
|
||||||
## 実践演習
|
## 実践演習
|
||||||
|
|
||||||
### 演習1: TODOアプリの履歴管理
|
### 演習1: TODOアプリの履歴管理
|
||||||
@@ -411,6 +730,34 @@ feat: TODO編集機能を追加
|
|||||||
変更
|
変更
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### 💡 コミットメッセージのフォーマット
|
||||||
|
|
||||||
|
**基本形:**
|
||||||
|
|
||||||
|
```
|
||||||
|
<タイプ>: <概要>
|
||||||
|
|
||||||
|
<詳細説明(オプション)>
|
||||||
|
```
|
||||||
|
|
||||||
|
**タイプの種類:**
|
||||||
|
|
||||||
|
- `feat`: 新機能
|
||||||
|
- `fix`: バグ修正
|
||||||
|
- `docs`: ドキュメント
|
||||||
|
- `style`: コードの意味に影響しない変更
|
||||||
|
- `refactor`: コードの改善
|
||||||
|
- `test`: テスト追加・修正
|
||||||
|
- `chore`: ビルドプロセスや補助ツールの変更
|
||||||
|
|
||||||
|
**例:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git commit -m "feat: TODOのフィルタリング機能を追加"
|
||||||
|
git commit -m "fix: 空のTODOを追加できるバグを修正"
|
||||||
|
git commit -m "docs: READMEにインストール手順を追加"
|
||||||
|
```
|
||||||
|
|
||||||
## VSCode ショートカット集
|
## VSCode ショートカット集
|
||||||
|
|
||||||
| 操作 | ショートカット |
|
| 操作 | ショートカット |
|
||||||
@@ -433,12 +780,73 @@ wsl --list --verbose
|
|||||||
cd ~/projects # Windows側ではなくWSL側で作業
|
cd ~/projects # Windows側ではなくWSL側で作業
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**💡 詳細な解決方法:**
|
||||||
|
|
||||||
|
**原因:** Windows側のファイルをWSL経由でアクセスすると遅い
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# ❌ 遅いパス(Windows側)
|
||||||
|
cd /mnt/c/Users/yourname/projects/
|
||||||
|
|
||||||
|
# ✅ 速いパス(WSL側)
|
||||||
|
cd ~/projects/
|
||||||
|
|
||||||
|
# パフォーマンス比較
|
||||||
|
time git status # 実行時間を計測
|
||||||
|
```
|
||||||
|
|
||||||
|
**他の改善策:**
|
||||||
|
|
||||||
|
1. Windows Defenderの除外フォルダにWSLを追加
|
||||||
|
2. `.gitconfig`に以下を追加:
|
||||||
|
|
||||||
|
```
|
||||||
|
[core]
|
||||||
|
preloadindex = true
|
||||||
|
fscache = true
|
||||||
|
```
|
||||||
|
|
||||||
### VSCodeがWSLに接続できない
|
### VSCodeがWSLに接続できない
|
||||||
|
|
||||||
1. WSL拡張機能の再インストール
|
1. WSL拡張機能の再インストール
|
||||||
2. WSLの再起動: `wsl --shutdown`
|
2. WSLの再起動: `wsl --shutdown`
|
||||||
3. VSCodeの再起動
|
3. VSCodeの再起動
|
||||||
|
|
||||||
|
**💡 詳細な解決手順:**
|
||||||
|
|
||||||
|
**ステップ1:拡張機能の確認**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# VSCodeで拡張機能を確認
|
||||||
|
# Ctrl+Shift+X で拡張機能パネルを開く
|
||||||
|
# "WSL" を検索してインストール済みか確認
|
||||||
|
```
|
||||||
|
|
||||||
|
**ステップ2:WSLの再起動**
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
# PowerShellで実行(管理者権限)
|
||||||
|
wsl --shutdown
|
||||||
|
|
||||||
|
# 再度WSLを起動
|
||||||
|
wsl
|
||||||
|
```
|
||||||
|
|
||||||
|
**ステップ3:コマンドラインからVSCodeを起動**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# WSL内から実行
|
||||||
|
cd ~/projects/todo-app
|
||||||
|
code .
|
||||||
|
```
|
||||||
|
|
||||||
|
**それでも動かない場合:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# WSL内でVSCode Serverを手動インストール
|
||||||
|
wget -O- https://code.visualstudio.com/sha/download?build=stable&os=cli-alpine-x64 | tar -xz
|
||||||
|
```
|
||||||
|
|
||||||
## 発展学習
|
## 発展学習
|
||||||
|
|
||||||
1. **Git Flow**の理解と実践
|
1. **Git Flow**の理解と実践
|
||||||
@@ -449,15 +857,138 @@ cd ~/projects # Windows側ではなくWSL側で作業
|
|||||||
|
|
||||||
## 推奨リソース
|
## 推奨リソース
|
||||||
|
|
||||||
|
### 📚 公式ドキュメント
|
||||||
|
|
||||||
- [Pro Git Book(日本語)](https://git-scm.com/book/ja/v2)
|
- [Pro Git Book(日本語)](https://git-scm.com/book/ja/v2)
|
||||||
- [Gitea公式ドキュメント](https://docs.gitea.io/)
|
- [Gitea公式ドキュメント](https://docs.gitea.io/)
|
||||||
- [VSCode公式ドキュメント - WSL](https://code.visualstudio.com/docs/remote/wsl)
|
- [VSCode公式ドキュメント - WSL](https://code.visualstudio.com/docs/remote/wsl)
|
||||||
- [Learn Git Branching(インタラクティブ学習)](https://learngitbranching.js.org/?locale=ja)
|
- [Learn Git Branching(インタラクティブ学習)](https://learngitbranching.js.org/?locale=ja)
|
||||||
|
|
||||||
|
### 🎥 日本語の動画教材
|
||||||
|
|
||||||
|
- [【Git入門】Gitの基本からGitHubの使い方まで完全解説](https://www.youtube.com/watch?v=LDOR5HfI_sQ)
|
||||||
|
- [【2024年版】VSCodeでGitを使う方法](https://www.youtube.com/watch?v=vMZ0C06soxA)
|
||||||
|
- [WSL2環境での開発環境構築](https://www.youtube.com/watch?v=CULr6fEUvAo)
|
||||||
|
- [Gitコマンド入門 - サルでもわかるGit入門](https://backlog.com/ja/git-tutorial/)
|
||||||
|
|
||||||
|
### 📖 日本語の参考記事・書籍
|
||||||
|
|
||||||
|
- [サルでもわかるGit入門](https://backlog.com/ja/git-tutorial/) - バックログの無料チュートリアル
|
||||||
|
- [Gitのしくみを図解で理解する](https://qiita.com/rana_kualu/items/4d2d75c6813c05034765)
|
||||||
|
- [いまさら聞けないGit入門](https://www.slideshare.net/matsukaz/git-28304397)
|
||||||
|
- 書籍:「Gitが、おもしろいほどわかる本」(MdN)
|
||||||
|
- 書籍:「エンジニアのためのGitの教科書」(翔泳社)
|
||||||
|
|
||||||
|
### 🛠️ 実践的な学習リソース
|
||||||
|
|
||||||
|
- [GitHub Skills](https://skills.github.com/) - GitHubのインタラクティブコース
|
||||||
|
- [Git Cheat Sheet (日本語)](https://training.github.com/downloads/ja/github-git-cheat-sheet/) - コマンド一覧
|
||||||
|
- [Gitea Demo](https://try.gitea.io/) - Giteaを試せるデモサイト
|
||||||
|
|
||||||
|
### 💻 WSL関連リソース
|
||||||
|
|
||||||
|
- [Microsoft WSL公式ドキュメント](https://learn.microsoft.com/ja-jp/windows/wsl/)
|
||||||
|
- [WSL2で作るWindows開発環境](https://zenn.dev/ryuu/articles/wsl2-dev-setup)
|
||||||
|
- [Windows Terminalの使い方](https://forest.watch.impress.co.jp/docs/serial/yajiuma/1265263.html)
|
||||||
|
|
||||||
|
## 📦 よくあるGitシナリオ集
|
||||||
|
|
||||||
|
### シナリオ1:変更を元に戻したい
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# まだコミットしていない変更を全て取り消す
|
||||||
|
git checkout -- .
|
||||||
|
|
||||||
|
# 特定のファイルだけ元に戻す
|
||||||
|
git checkout -- src/app/page.tsx
|
||||||
|
|
||||||
|
# ステージングした変更を取り消す
|
||||||
|
git reset HEAD filename
|
||||||
|
```
|
||||||
|
|
||||||
|
### シナリオ2:コミット履歴をきれいにしたい
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 直前のコミットと統合(コミットをまとめる)
|
||||||
|
git reset --soft HEAD~1
|
||||||
|
git commit -m "整理されたメッセージ"
|
||||||
|
|
||||||
|
# 注意:すでにpushした場合は他の人に影響するため避ける
|
||||||
|
```
|
||||||
|
|
||||||
|
### シナリオ3:作業中に別の作業が必要になった
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 現在の作業を一時保存
|
||||||
|
git stash save "途中のTODO編集機能"
|
||||||
|
|
||||||
|
# 別のブランチで緊急作業
|
||||||
|
git checkout -b hotfix/urgent-bug
|
||||||
|
# ... 作業 ...
|
||||||
|
git commit -m "fix: 緊急バグ修正"
|
||||||
|
|
||||||
|
# 元のブランチに戻って作業再開
|
||||||
|
git checkout feature/todo-edit
|
||||||
|
git stash pop
|
||||||
|
```
|
||||||
|
|
||||||
|
## 👨🏫 学習のアドバイス
|
||||||
|
|
||||||
|
### Git初心者が陥りやすい罠
|
||||||
|
|
||||||
|
1. **いきなりmainブランチで作業**
|
||||||
|
- 必ずブランチを作成してから作業
|
||||||
|
- mainブランチは「完成品」を置く場所
|
||||||
|
|
||||||
|
2. **コミットメッセージが適当**
|
||||||
|
- 「あとで見る自分」のために丁寧に書く
|
||||||
|
- 「何を」「なぜ」変更したかを記載
|
||||||
|
|
||||||
|
3. **pushしてからミスに気づく**
|
||||||
|
- push前に`git log`で確認
|
||||||
|
- `git diff origin/main`で差分を確認
|
||||||
|
|
||||||
|
### 効率的な学習方法
|
||||||
|
|
||||||
|
1. **コマンドとGUIを両方使う**
|
||||||
|
- 基本はVSCodeのGUIでOK
|
||||||
|
- 複雑な操作はコマンドを学ぶ
|
||||||
|
|
||||||
|
2. **小さなコミットを心がける**
|
||||||
|
- 1機能1コミット
|
||||||
|
- 後から振り返りやすい
|
||||||
|
|
||||||
|
3. **エラーを恐れない**
|
||||||
|
- Gitは「元に戻す」が得意
|
||||||
|
- 大抵のミスは修復可能
|
||||||
|
|
||||||
|
### 次のステップ
|
||||||
|
|
||||||
|
1. **ブランチ戦略を学ぶ**
|
||||||
|
- Git Flow
|
||||||
|
- GitHub Flow
|
||||||
|
- GitLab Flow
|
||||||
|
|
||||||
|
2. **チーム開発のルール**
|
||||||
|
- コードレビュー
|
||||||
|
- コミットメッセージ規約
|
||||||
|
- ブランチ保護
|
||||||
|
|
||||||
|
3. **自動化を学ぶ**
|
||||||
|
- Git Hooks
|
||||||
|
- CI/CDパイプライン
|
||||||
|
|
||||||
## まとめ
|
## まとめ
|
||||||
|
|
||||||
WSL+VSCode環境でのGit/Gitea学習は、Windows環境でLinuxライクな開発を可能にします。VSCodeの強力なGit統合機能を活用しながら、コマンドラインでの操作も習得することで、柔軟な開発スタイルを身につけることができます。
|
WSL+VSCode環境でのGit/Gitea学習は、Windows環境でLinuxライクな開発を可能にします。VSCodeの強力なGit統合機能を活用しながら、コマンドラインでの操作も習得することで、柔軟な開発スタイルを身につけることができます。
|
||||||
|
|
||||||
|
**大切なのは:**
|
||||||
|
|
||||||
|
- 🔄 失敗を恐れず、何度も試す
|
||||||
|
- 📝 学んだことをメモする
|
||||||
|
- 🤝 わからないことは質問する
|
||||||
|
- 🎯 小さな目標から始める
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
このカリキュラムは、TODOアプリ開発カリキュラムと連携して使用することを想定しています。
|
このカリキュラムは、TODOアプリ開発カリキュラムと連携して使用することを想定しています。
|
||||||
|
|||||||
8
src/app/components/HelloWorld.tsx
Normal file
8
src/app/components/HelloWorld.tsx
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
export default function HelloWorld() {
|
||||||
|
return (
|
||||||
|
<div className="p-4 bg-blue-100 rounded">
|
||||||
|
<h2 className="text-xl font-bold">Hello World!</h2>
|
||||||
|
<p className="text-gray-700">Reactコンポーネントの第一歩です</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
542
src/app/page.tsx
542
src/app/page.tsx
@@ -1,103 +1,451 @@
|
|||||||
import Image from "next/image";
|
'use client';
|
||||||
|
|
||||||
|
import { useState, useEffect, useCallback, useMemo } from 'react';
|
||||||
|
import TodoList from '@/components/TodoList';
|
||||||
|
import FilterButtons from '@/components/FilterButtons';
|
||||||
|
|
||||||
|
// Todo型の定義
|
||||||
|
type Todo = {
|
||||||
|
id: number;
|
||||||
|
text: string;
|
||||||
|
completed: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
type FilterType = 'all' | 'active' | 'completed';
|
||||||
|
|
||||||
|
// エラー型の定義
|
||||||
|
type AppError = {
|
||||||
|
type: 'STORAGE_ERROR' | 'IMPORT_ERROR' | 'VALIDATION_ERROR';
|
||||||
|
message: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 型ガード関数
|
||||||
|
const isTodo = (obj: unknown): obj is Todo => {
|
||||||
|
return (
|
||||||
|
obj !== null &&
|
||||||
|
typeof obj === 'object' &&
|
||||||
|
typeof (obj as any).id === 'number' &&
|
||||||
|
typeof (obj as any).text === 'string' &&
|
||||||
|
typeof (obj as any).completed === 'boolean'
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const isTodoArray = (data: unknown): data is Todo[] => {
|
||||||
|
return Array.isArray(data) && data.every(isTodo);
|
||||||
|
};
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
return (
|
// Local Storage のキー
|
||||||
<div className="grid grid-rows-[20px_1fr_20px] items-center justify-items-center min-h-screen p-8 pb-20 gap-16 sm:p-20 font-[family-name:var(--font-geist-sans)]">
|
const STORAGE_KEY = 'todo-app-data';
|
||||||
<main className="flex flex-col gap-[32px] row-start-2 items-center sm:items-start">
|
|
||||||
<Image
|
|
||||||
className="dark:invert"
|
|
||||||
src="/next.svg"
|
|
||||||
alt="Next.js logo"
|
|
||||||
width={180}
|
|
||||||
height={38}
|
|
||||||
priority
|
|
||||||
/>
|
|
||||||
<ol className="list-inside list-decimal text-sm/6 text-center sm:text-left font-[family-name:var(--font-geist-mono)]">
|
|
||||||
<li className="mb-2 tracking-[-.01em]">
|
|
||||||
Get started by editing{" "}
|
|
||||||
<code className="bg-black/[.05] dark:bg-white/[.06] px-1 py-0.5 rounded font-[family-name:var(--font-geist-mono)] font-semibold">
|
|
||||||
src/app/page.tsx
|
|
||||||
</code>
|
|
||||||
.
|
|
||||||
</li>
|
|
||||||
<li className="tracking-[-.01em]">
|
|
||||||
Save and see your changes instantly.
|
|
||||||
</li>
|
|
||||||
</ol>
|
|
||||||
|
|
||||||
<div className="flex gap-4 items-center flex-col sm:flex-row">
|
// 状態管理
|
||||||
<a
|
const [todos, setTodos] = useState<Todo[]>([]);
|
||||||
className="rounded-full border border-solid border-transparent transition-colors flex items-center justify-center bg-foreground text-background gap-2 hover:bg-[#383838] dark:hover:bg-[#ccc] font-medium text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5 sm:w-auto"
|
const [inputValue, setInputValue] = useState<string>('');
|
||||||
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
const [showConfirm, setShowConfirm] = useState(false);
|
||||||
target="_blank"
|
const [targetId, setTargetId] = useState<number | null>(null);
|
||||||
rel="noopener noreferrer"
|
const [showInputAlert, setShowInputAlert] = useState(false);
|
||||||
>
|
const [editingId, setEditingId] = useState<number | null>(null);
|
||||||
<Image
|
const [editText, setEditText] = useState('');
|
||||||
className="dark:invert"
|
const [filter, setFilter] = useState<FilterType>('all');
|
||||||
src="/vercel.svg"
|
|
||||||
alt="Vercel logomark"
|
// エラー状態の管理
|
||||||
width={20}
|
const [error, setError] = useState<AppError | null>(null);
|
||||||
height={20}
|
const [showErrorDialog, setShowErrorDialog] = useState(false);
|
||||||
/>
|
|
||||||
Deploy now
|
// フィルターに応じたTodoの数をメモ化
|
||||||
</a>
|
const { activeCount, completedCount } = useMemo(() => ({
|
||||||
<a
|
activeCount: todos.filter(todo => !todo.completed).length,
|
||||||
className="rounded-full border border-solid border-black/[.08] dark:border-white/[.145] transition-colors flex items-center justify-center hover:bg-[#f2f2f2] dark:hover:bg-[#1a1a1a] hover:border-transparent font-medium text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5 w-full sm:w-auto md:w-[158px]"
|
completedCount: todos.filter(todo => todo.completed).length,
|
||||||
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
}), [todos]);
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
// エラー処理用のヘルパー関数をメモ化
|
||||||
>
|
const handleError = useCallback((error: AppError) => {
|
||||||
Read our docs
|
console.error('アプリケーションエラー:', error);
|
||||||
</a>
|
setError(error);
|
||||||
|
setShowErrorDialog(true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// エラーダイアログを閉じる
|
||||||
|
const closeErrorDialog = useCallback(() => {
|
||||||
|
setShowErrorDialog(false);
|
||||||
|
setError(null);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// バリデーション関数をメモ化
|
||||||
|
const validateTodoText = useCallback((text: string): boolean => {
|
||||||
|
if (!text || text.trim().length === 0) {
|
||||||
|
handleError({
|
||||||
|
type: 'VALIDATION_ERROR',
|
||||||
|
message: 'TODOの内容を入力してください'
|
||||||
|
});
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (text.trim().length > 100) {
|
||||||
|
handleError({
|
||||||
|
type: 'VALIDATION_ERROR',
|
||||||
|
message: 'TODOは100文字以内で入力してください'
|
||||||
|
});
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}, [handleError]);
|
||||||
|
|
||||||
|
// Local Storage操作をメモ化
|
||||||
|
const safeLocalStorageGet = useCallback((key: string): string | null => {
|
||||||
|
try {
|
||||||
|
return localStorage.getItem(key);
|
||||||
|
} catch (error) {
|
||||||
|
handleError({
|
||||||
|
type: 'STORAGE_ERROR',
|
||||||
|
message: 'データの読み込みに失敗しました'
|
||||||
|
});
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}, [handleError]);
|
||||||
|
|
||||||
|
const safeLocalStorageSet = useCallback((key: string, value: string): boolean => {
|
||||||
|
try {
|
||||||
|
localStorage.setItem(key, value);
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
handleError({
|
||||||
|
type: 'STORAGE_ERROR',
|
||||||
|
message: 'データの保存に失敗しました'
|
||||||
|
});
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}, [handleError]);
|
||||||
|
|
||||||
|
// 編集関連の関数をメモ化
|
||||||
|
const startEdit = useCallback((todo: Todo) => {
|
||||||
|
setEditingId(todo.id);
|
||||||
|
setEditText(todo.text);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const saveEdit = useCallback(() => {
|
||||||
|
if (!validateTodoText(editText)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const updatedTodos = todos.map(todo => {
|
||||||
|
if (todo.id === editingId) {
|
||||||
|
return { ...todo, text: editText.trim() };
|
||||||
|
}
|
||||||
|
return todo;
|
||||||
|
});
|
||||||
|
|
||||||
|
setTodos(updatedTodos);
|
||||||
|
setEditingId(null);
|
||||||
|
setEditText('');
|
||||||
|
} catch (error) {
|
||||||
|
handleError({
|
||||||
|
type: 'VALIDATION_ERROR',
|
||||||
|
message: 'TODOの更新に失敗しました'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [editText, editingId, todos, validateTodoText, handleError]);
|
||||||
|
|
||||||
|
const cancelEdit = useCallback(() => {
|
||||||
|
setEditingId(null);
|
||||||
|
setEditText('');
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// TODO操作の関数をメモ化
|
||||||
|
const addTodo = useCallback(() => {
|
||||||
|
if (!validateTodoText(inputValue)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const newTodo: Todo = {
|
||||||
|
id: Date.now(),
|
||||||
|
text: inputValue.trim(),
|
||||||
|
completed: false
|
||||||
|
};
|
||||||
|
setTodos(prev => [...prev, newTodo]);
|
||||||
|
setInputValue('');
|
||||||
|
} catch (error) {
|
||||||
|
handleError({
|
||||||
|
type: 'VALIDATION_ERROR',
|
||||||
|
message: 'TODOの追加に失敗しました'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [inputValue, validateTodoText, handleError]);
|
||||||
|
|
||||||
|
const handleSubmit = useCallback((e: React.FormEvent<HTMLFormElement>) => {
|
||||||
|
e.preventDefault();
|
||||||
|
addTodo();
|
||||||
|
}, [addTodo]);
|
||||||
|
|
||||||
|
const toggleTodo = useCallback((id: number) => {
|
||||||
|
try {
|
||||||
|
setTodos(prev => prev.map(todo => {
|
||||||
|
if (todo.id === id) {
|
||||||
|
return { ...todo, completed: !todo.completed };
|
||||||
|
}
|
||||||
|
return todo;
|
||||||
|
}));
|
||||||
|
} catch (error) {
|
||||||
|
handleError({
|
||||||
|
type: 'VALIDATION_ERROR',
|
||||||
|
message: 'TODO状態の更新に失敗しました'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [handleError]);
|
||||||
|
|
||||||
|
const handleDeleteClick = useCallback((id: number) => {
|
||||||
|
setShowConfirm(true);
|
||||||
|
setTargetId(id);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const confirmDelete = useCallback(() => {
|
||||||
|
if (targetId !== null) {
|
||||||
|
try {
|
||||||
|
setTodos(prev => prev.filter(todo => todo.id !== targetId));
|
||||||
|
setShowConfirm(false);
|
||||||
|
setTargetId(null);
|
||||||
|
} catch (error) {
|
||||||
|
handleError({
|
||||||
|
type: 'VALIDATION_ERROR',
|
||||||
|
message: 'TODOの削除に失敗しました'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [targetId, handleError]);
|
||||||
|
|
||||||
|
const cancelDelete = useCallback(() => {
|
||||||
|
setShowConfirm(false);
|
||||||
|
setTargetId(null);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const closeInputAlert = useCallback(() => {
|
||||||
|
setShowInputAlert(false);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const clearCompleted = useCallback(() => {
|
||||||
|
try {
|
||||||
|
setTodos(prev => prev.filter(todo => !todo.completed));
|
||||||
|
} catch (error) {
|
||||||
|
handleError({
|
||||||
|
type: 'VALIDATION_ERROR',
|
||||||
|
message: '完了済みTODOの削除に失敗しました'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [handleError]);
|
||||||
|
|
||||||
|
// エクスポート・インポート機能をメモ化
|
||||||
|
const exportData = useCallback(() => {
|
||||||
|
try {
|
||||||
|
const dataStr = JSON.stringify(todos, null, 2);
|
||||||
|
const dataBlob = new Blob([dataStr], { type: 'application/json' });
|
||||||
|
const url = URL.createObjectURL(dataBlob);
|
||||||
|
const link = document.createElement('a');
|
||||||
|
link.href = url;
|
||||||
|
link.download = 'todos.json';
|
||||||
|
link.click();
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
} catch (error) {
|
||||||
|
handleError({
|
||||||
|
type: 'STORAGE_ERROR',
|
||||||
|
message: 'データのエクスポートに失敗しました'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [todos, handleError]);
|
||||||
|
|
||||||
|
// 型安全なインポート機能
|
||||||
|
const importData = useCallback((event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const file = event.target.files?.[0];
|
||||||
|
if (!file) return;
|
||||||
|
|
||||||
|
const reader = new FileReader();
|
||||||
|
|
||||||
|
reader.onload = (e: ProgressEvent<FileReader>) => {
|
||||||
|
try {
|
||||||
|
const result = e.target?.result;
|
||||||
|
if (typeof result !== 'string') {
|
||||||
|
throw new Error('ファイルの読み込みに失敗しました');
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsedData: unknown = JSON.parse(result);
|
||||||
|
|
||||||
|
if (!isTodoArray(parsedData)) {
|
||||||
|
throw new Error('無効なTODOデータ形式です');
|
||||||
|
}
|
||||||
|
|
||||||
|
setTodos(parsedData);
|
||||||
|
event.target.value = '';
|
||||||
|
} catch (error) {
|
||||||
|
const errorMessage = error instanceof Error ? error.message : '不明なエラー';
|
||||||
|
handleError({
|
||||||
|
type: 'IMPORT_ERROR',
|
||||||
|
message: `ファイルの読み込みに失敗しました: ${errorMessage}`
|
||||||
|
});
|
||||||
|
event.target.value = '';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
reader.onerror = () => {
|
||||||
|
handleError({
|
||||||
|
type: 'IMPORT_ERROR',
|
||||||
|
message: 'ファイルの読み込み中にエラーが発生しました'
|
||||||
|
});
|
||||||
|
event.target.value = '';
|
||||||
|
};
|
||||||
|
|
||||||
|
reader.readAsText(file);
|
||||||
|
}, [handleError]);
|
||||||
|
|
||||||
|
// 型安全なLocal Storage読み込み
|
||||||
|
useEffect(() => {
|
||||||
|
const savedTodos = safeLocalStorageGet(STORAGE_KEY);
|
||||||
|
if (savedTodos) {
|
||||||
|
try {
|
||||||
|
const parsedData: unknown = JSON.parse(savedTodos);
|
||||||
|
|
||||||
|
if (isTodoArray(parsedData)) {
|
||||||
|
setTodos(parsedData);
|
||||||
|
} else {
|
||||||
|
throw new Error('保存されたデータの形式が正しくありません');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
const errorMessage = error instanceof Error ? error.message : '不明なエラー';
|
||||||
|
handleError({
|
||||||
|
type: 'STORAGE_ERROR',
|
||||||
|
message: `データ読み込みエラー: ${errorMessage}`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [safeLocalStorageGet, handleError]);
|
||||||
|
|
||||||
|
// TODOが変更されるたびに保存(デバウンス処理付き)
|
||||||
|
useEffect(() => {
|
||||||
|
const timeoutId = setTimeout(() => {
|
||||||
|
safeLocalStorageSet(STORAGE_KEY, JSON.stringify(todos));
|
||||||
|
}, 300); // 300ms後に保存
|
||||||
|
|
||||||
|
return () => clearTimeout(timeoutId);
|
||||||
|
}, [todos, safeLocalStorageSet]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<main className="min-h-screen p-8 bg-gray-50">
|
||||||
|
<div className="max-w-md mx-auto bg-white rounded-lg shadow-md p-6">
|
||||||
|
{/* ヘッダー部分 */}
|
||||||
|
<div className="flex justify-between items-center mb-6">
|
||||||
|
<h1 className="text-2xl font-bold text-gray-800">TODOアプリ</h1>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<button
|
||||||
|
onClick={exportData}
|
||||||
|
className="text-blue-500 hover:text-blue-700 px-3 py-1 rounded transition-colors text-sm"
|
||||||
|
>
|
||||||
|
エクスポート
|
||||||
|
</button>
|
||||||
|
<label className="text-blue-500 hover:text-blue-700 px-3 py-1 rounded transition-colors cursor-pointer text-sm">
|
||||||
|
インポート
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
accept=".json"
|
||||||
|
onChange={importData}
|
||||||
|
className="hidden"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
|
||||||
<footer className="row-start-3 flex gap-[24px] flex-wrap items-center justify-center">
|
{/* 入力フォーム */}
|
||||||
<a
|
<form onSubmit={handleSubmit} className="mb-4">
|
||||||
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
|
<input
|
||||||
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
type="text"
|
||||||
target="_blank"
|
value={inputValue}
|
||||||
rel="noopener noreferrer"
|
onChange={(e) => setInputValue(e.target.value)}
|
||||||
>
|
placeholder="TODOを入力してEnterキー"
|
||||||
<Image
|
className="w-full p-3 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||||
aria-hidden
|
maxLength={100}
|
||||||
src="/file.svg"
|
|
||||||
alt="File icon"
|
|
||||||
width={16}
|
|
||||||
height={16}
|
|
||||||
/>
|
/>
|
||||||
Learn
|
<button
|
||||||
</a>
|
type="submit"
|
||||||
<a
|
className="w-full mt-2 bg-blue-500 text-white p-3 rounded-lg hover:bg-blue-600 transition-colors"
|
||||||
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
|
>
|
||||||
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
追加
|
||||||
target="_blank"
|
</button>
|
||||||
rel="noopener noreferrer"
|
</form>
|
||||||
>
|
|
||||||
<Image
|
{/* フィルターボタン */}
|
||||||
aria-hidden
|
<FilterButtons
|
||||||
src="/window.svg"
|
filter={filter}
|
||||||
alt="Window icon"
|
totalCount={todos.length}
|
||||||
width={16}
|
activeCount={activeCount}
|
||||||
height={16}
|
completedCount={completedCount}
|
||||||
/>
|
onFilterChange={setFilter}
|
||||||
Examples
|
onClearCompleted={clearCompleted}
|
||||||
</a>
|
/>
|
||||||
<a
|
|
||||||
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
|
{/* TODOリスト */}
|
||||||
href="https://nextjs.org?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
<TodoList
|
||||||
target="_blank"
|
todos={todos}
|
||||||
rel="noopener noreferrer"
|
filter={filter}
|
||||||
>
|
editingId={editingId}
|
||||||
<Image
|
editText={editText}
|
||||||
aria-hidden
|
onToggleTodo={toggleTodo}
|
||||||
src="/globe.svg"
|
onStartEdit={startEdit}
|
||||||
alt="Globe icon"
|
onSaveEdit={saveEdit}
|
||||||
width={16}
|
onCancelEdit={cancelEdit}
|
||||||
height={16}
|
onDeleteClick={handleDeleteClick}
|
||||||
/>
|
onEditTextChange={setEditText}
|
||||||
Go to nextjs.org →
|
/>
|
||||||
</a>
|
|
||||||
</footer>
|
{/* 確認ダイアログ */}
|
||||||
</div>
|
{showConfirm && (
|
||||||
|
<div className="fixed inset-0 flex items-center justify-center bg-black bg-opacity-50 z-50">
|
||||||
|
<div className="bg-white p-6 rounded shadow">
|
||||||
|
<p className="mb-4">本当に削除しますか?</p>
|
||||||
|
<button onClick={confirmDelete} className="mr-2 px-4 py-2 bg-red-500 text-white rounded">はい</button>
|
||||||
|
<button onClick={cancelDelete} className="px-4 py-2 bg-gray-300 rounded">いいえ</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 空欄入力時のカスタムダイアログ */}
|
||||||
|
{showInputAlert && (
|
||||||
|
<div className="fixed inset-0 flex items-center justify-center bg-black bg-opacity-50 z-50">
|
||||||
|
<div className="bg-white p-6 rounded shadow flex flex-col items-center">
|
||||||
|
<p className="mb-4">TODOを入力してください</p>
|
||||||
|
<button
|
||||||
|
onClick={closeInputAlert}
|
||||||
|
className="w-full px-4 py-2 bg-blue-500 text-white rounded text-center"
|
||||||
|
style={{ maxWidth: "240px" }}
|
||||||
|
>
|
||||||
|
閉じる
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* エラーダイアログ */}
|
||||||
|
{showErrorDialog && error && (
|
||||||
|
<div className="fixed inset-0 flex items-center justify-center bg-black bg-opacity-50 z-50">
|
||||||
|
<div className="bg-white p-6 rounded shadow max-w-sm mx-4">
|
||||||
|
<div className="flex items-center mb-4">
|
||||||
|
<div className="bg-red-100 p-2 rounded-full mr-3">
|
||||||
|
<svg className="w-6 h-6 text-red-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.732-.833-2.5 0L3.732 16.5c-.77.833.192 2.5 1.732 2.5z" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<h3 className="text-lg font-semibold text-gray-900">エラー</h3>
|
||||||
|
</div>
|
||||||
|
<p className="mb-4 text-gray-700">{error.message}</p>
|
||||||
|
<button
|
||||||
|
onClick={closeErrorDialog}
|
||||||
|
className="w-full px-4 py-2 bg-red-500 text-white rounded hover:bg-red-600 transition-colors"
|
||||||
|
>
|
||||||
|
閉じる
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
69
src/components/FilterButtons.tsx
Normal file
69
src/components/FilterButtons.tsx
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
type FilterType = 'all' | 'active' | 'completed';
|
||||||
|
|
||||||
|
type FilterButtonsProps = {
|
||||||
|
filter: FilterType;
|
||||||
|
totalCount: number;
|
||||||
|
activeCount: number;
|
||||||
|
completedCount: number;
|
||||||
|
onFilterChange: (filter: FilterType) => void;
|
||||||
|
onClearCompleted: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const FilterButtons = React.memo(function FilterButtons({
|
||||||
|
filter,
|
||||||
|
totalCount,
|
||||||
|
activeCount,
|
||||||
|
completedCount,
|
||||||
|
onFilterChange,
|
||||||
|
onClearCompleted,
|
||||||
|
}: FilterButtonsProps) {
|
||||||
|
return (
|
||||||
|
<div className="flex justify-between items-center mb-4 p-4 bg-gray-100 rounded-lg">
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<button
|
||||||
|
onClick={() => onFilterChange('all')}
|
||||||
|
className={`px-4 py-2 rounded transition-colors ${
|
||||||
|
filter === 'all'
|
||||||
|
? 'bg-blue-500 text-white'
|
||||||
|
: 'bg-white text-gray-700 hover:bg-gray-200'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
すべて ({totalCount})
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => onFilterChange('active')}
|
||||||
|
className={`px-4 py-2 rounded transition-colors ${
|
||||||
|
filter === 'active'
|
||||||
|
? 'bg-blue-500 text-white'
|
||||||
|
: 'bg-white text-gray-700 hover:bg-gray-200'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
未完了 ({activeCount})
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => onFilterChange('completed')}
|
||||||
|
className={`px-4 py-2 rounded transition-colors ${
|
||||||
|
filter === 'completed'
|
||||||
|
? 'bg-blue-500 text-white'
|
||||||
|
: 'bg-white text-gray-700 hover:bg-gray-200'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
完了 ({completedCount})
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{completedCount > 0 && (
|
||||||
|
<button
|
||||||
|
onClick={onClearCompleted}
|
||||||
|
className="text-red-500 hover:text-red-700 px-3 py-1 rounded transition-colors"
|
||||||
|
>
|
||||||
|
完了済みを削除
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
export default FilterButtons;
|
||||||
105
src/components/TodoItem.tsx
Normal file
105
src/components/TodoItem.tsx
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
type Todo = {
|
||||||
|
id: number;
|
||||||
|
text: string;
|
||||||
|
completed: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
type TodoItemProps = {
|
||||||
|
todo: Todo;
|
||||||
|
isEditing: boolean;
|
||||||
|
editText: string;
|
||||||
|
onToggle: () => void;
|
||||||
|
onStartEdit: () => void;
|
||||||
|
onSaveEdit: () => void;
|
||||||
|
onCancelEdit: () => void;
|
||||||
|
onDelete: () => void;
|
||||||
|
onEditTextChange: (text: string) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const TodoItem = React.memo(function TodoItem({
|
||||||
|
todo,
|
||||||
|
isEditing,
|
||||||
|
editText,
|
||||||
|
onToggle,
|
||||||
|
onStartEdit,
|
||||||
|
onSaveEdit,
|
||||||
|
onCancelEdit,
|
||||||
|
onDelete,
|
||||||
|
onEditTextChange,
|
||||||
|
}: TodoItemProps) {
|
||||||
|
const handleKeyDown = (e: React.KeyboardEvent) => {
|
||||||
|
if (e.key === 'Enter') onSaveEdit();
|
||||||
|
if (e.key === 'Escape') onCancelEdit();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex items-center p-3 bg-gray-50 rounded-lg hover:bg-gray-100 transition-colors">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={todo.completed}
|
||||||
|
onChange={onToggle}
|
||||||
|
className="mr-3 h-5 w-5 cursor-pointer"
|
||||||
|
/>
|
||||||
|
|
||||||
|
{isEditing ? (
|
||||||
|
// 編集モード
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={editText}
|
||||||
|
onChange={(e) => onEditTextChange(e.target.value)}
|
||||||
|
onKeyDown={handleKeyDown}
|
||||||
|
className="flex-1 border border-blue-500 p-2 rounded focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||||
|
autoFocus
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
// 通常モード
|
||||||
|
<span
|
||||||
|
onClick={onStartEdit}
|
||||||
|
className={`flex-1 cursor-pointer p-2 rounded hover:bg-gray-200 transition-colors ${
|
||||||
|
todo.completed ? 'line-through text-gray-500' : 'text-gray-800'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{todo.text}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{isEditing ? (
|
||||||
|
// 編集モードのボタン
|
||||||
|
<div className="flex gap-2 ml-2">
|
||||||
|
<button
|
||||||
|
onClick={onSaveEdit}
|
||||||
|
className="text-green-600 hover:text-green-800 px-2 py-1 rounded transition-colors"
|
||||||
|
>
|
||||||
|
保存
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={onCancelEdit}
|
||||||
|
className="text-gray-600 hover:text-gray-800 px-2 py-1 rounded transition-colors"
|
||||||
|
>
|
||||||
|
キャンセル
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
// 通常モードのボタン
|
||||||
|
<div className="flex gap-2 ml-2">
|
||||||
|
<button
|
||||||
|
onClick={onStartEdit}
|
||||||
|
className="text-blue-500 hover:text-blue-700 px-2 py-1 rounded transition-colors"
|
||||||
|
>
|
||||||
|
編集
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={onDelete}
|
||||||
|
className="text-red-500 hover:text-red-700 px-2 py-1 rounded transition-colors"
|
||||||
|
>
|
||||||
|
削除
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
export default TodoItem;
|
||||||
77
src/components/TodoList.tsx
Normal file
77
src/components/TodoList.tsx
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
import React, { useMemo } from 'react';
|
||||||
|
import TodoItem from './TodoItem';
|
||||||
|
|
||||||
|
type Todo = {
|
||||||
|
id: number;
|
||||||
|
text: string;
|
||||||
|
completed: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
type FilterType = 'all' | 'active' | 'completed';
|
||||||
|
|
||||||
|
type TodoListProps = {
|
||||||
|
todos: Todo[];
|
||||||
|
filter: FilterType;
|
||||||
|
editingId: number | null;
|
||||||
|
editText: string;
|
||||||
|
onToggleTodo: (id: number) => void;
|
||||||
|
onStartEdit: (todo: Todo) => void;
|
||||||
|
onSaveEdit: () => void;
|
||||||
|
onCancelEdit: () => void;
|
||||||
|
onDeleteClick: (id: number) => void;
|
||||||
|
onEditTextChange: (text: string) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const TodoList = React.memo(function TodoList({
|
||||||
|
todos,
|
||||||
|
filter,
|
||||||
|
editingId,
|
||||||
|
editText,
|
||||||
|
onToggleTodo,
|
||||||
|
onStartEdit,
|
||||||
|
onSaveEdit,
|
||||||
|
onCancelEdit,
|
||||||
|
onDeleteClick,
|
||||||
|
onEditTextChange,
|
||||||
|
}: TodoListProps) {
|
||||||
|
// フィルタリング処理をメモ化
|
||||||
|
const filteredTodos = useMemo(() => {
|
||||||
|
switch (filter) {
|
||||||
|
case 'active':
|
||||||
|
return todos.filter(todo => !todo.completed);
|
||||||
|
case 'completed':
|
||||||
|
return todos.filter(todo => todo.completed);
|
||||||
|
default:
|
||||||
|
return todos;
|
||||||
|
}
|
||||||
|
}, [todos, filter]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-2">
|
||||||
|
{filteredTodos.length === 0 ? (
|
||||||
|
<p className="text-gray-500 text-center py-8">
|
||||||
|
{filter === 'active' && 'すべて完了しました!'}
|
||||||
|
{filter === 'completed' && '完了したTODOがありません'}
|
||||||
|
{filter === 'all' && 'TODOがありません'}
|
||||||
|
</p>
|
||||||
|
) : (
|
||||||
|
filteredTodos.map((todo) => (
|
||||||
|
<TodoItem
|
||||||
|
key={todo.id}
|
||||||
|
todo={todo}
|
||||||
|
isEditing={editingId === todo.id}
|
||||||
|
editText={editText}
|
||||||
|
onToggle={() => onToggleTodo(todo.id)}
|
||||||
|
onStartEdit={() => onStartEdit(todo)}
|
||||||
|
onSaveEdit={onSaveEdit}
|
||||||
|
onCancelEdit={onCancelEdit}
|
||||||
|
onDelete={() => onDeleteClick(todo.id)}
|
||||||
|
onEditTextChange={onEditTextChange}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
export default TodoList;
|
||||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user