18 Commits

Author SHA1 Message Date
253d25ab5c 型安全性を限界まで堅牢に 2025-09-29 19:41:02 +09:00
f7d9950bbc step5-3 最適化まで終了 2025-09-29 09:06:45 +09:00
d387a00dff エラーハンドリングまでを追記 2025-09-29 06:13:59 +09:00
f379dd5d8f tep 5.1: コンポーネントの分割まで完了 2025-09-29 02:48:02 +09:00
1b92843a85 pahse4-3まで データを永続化 2025-09-29 02:22:24 +09:00
21b87f0756 phase4-2まで 2025-07-29 07:15:11 +09:00
e29e4ff796 編集機能追加 2025-07-28 03:48:47 +09:00
67cd70bbd6 さらにコメントアウトのカバー範囲を拡充 2025-07-13 20:22:18 +09:00
33bf23aac4 コメントアウトをより具体的に修正 2025-07-13 20:12:08 +09:00
62e2942cb9 TODOを空白で登録しようとした際のダイアログをカスタム化+UI調整 2025-07-12 17:33:40 +09:00
cd0e6cca9f コード全体に質疑応答補助用のコメントアウトを追加 2025-07-12 17:25:15 +09:00
f51fbe074a todo削除機能追加 2025-07-12 16:49:23 +09:00
437dcaf27c HalloWorldコンポーネントを試験的に追加 2025-07-12 16:43:59 +09:00
9ff988db7a todoセット機能追加+todo完了機能追加 2025-07-12 16:43:37 +09:00
97d0606a16 TODOアプリ開発カリキュラムを大幅に充実
- 開発環境セットアップセクションを追加(Node.js、VS Code、Git)
- 各Phaseに詳細な解説と日本語参考文献を追加
- コード例を充実させ、「なぜそう書くのか」の説明を追加
- よくあるエラーと対処法、FAQ(よくある質問)セクションを追加
- 段階的な学習目標と確認ポイントを明確化
- トラブルシューティングとデバッグ方法を充実
- パフォーマンス最適化とベストプラクティスまで網羅
2025-07-08 16:11:22 +09:00
c017844b9d ドキュメント更新 2025-07-08 16:11:22 +09:00
72fdc8bdbe トップページを1から作り直し 2025-07-01 16:05:23 +09:00
791411e209 学生向けREADMEに更新
- デフォルトのNext.jsテンプレートからカリキュラム用ガイドに変更
- TODOアプリ開発とGit/Gitea学習の全体像を提示
- 初学者向けの学習手順、つまずきポイント、FAQ等を追加
2025-07-01 03:44:15 +09:00
8 changed files with 3113 additions and 195 deletions

270
README.md
View File

@@ -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
yarn dev
# or
pnpm dev
# or
bun dev
### 🚀 学べる技術スキル
#### 1. **モダンなWeb開発技術**
- **Next.js 15** - 最新のReactフレームワーク
- **TypeScript** - 型安全なJavaScript
- **Tailwind CSS v4** - 効率的なスタイリング
- **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.
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
```bash
# 開発サーバーを起動
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! 🎉**

View File

@@ -102,6 +102,35 @@ git init
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での確認:
- 左サイドバーの「ソース管理」アイコンをクリック
@@ -122,6 +151,44 @@ git status
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方式
1. ソース管理パネルを開くCtrl+Shift+G
@@ -142,6 +209,37 @@ git log --oneline
git log --graph --oneline --all
```
**💡 コマンドの詳細説明:**
**git logの様々な表示方法**
```bash
# 詳細表示(デフォルト)
$ git log
commit a1b2c3d4e5f6... # コミットIDSHA-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:
- Git Graph拡張機能でビジュアル表示
@@ -169,6 +267,42 @@ git branch
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:
- ステータスバー左下のブランチ名をクリック
@@ -186,6 +320,44 @@ git checkout main
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でのマージ:
1. Git Graphで視覚的にブランチを確認
@@ -356,11 +528,76 @@ git config --global alias.lg "log --graph --oneline --all"
- **問題**: Windows側のパスとWSL側のパスの混同
- **対策**: 常に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. 改行コードの不一致
- **問題**: WindowsのCRLFとLinuxの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が重い
- **問題**: Windows側のファイルをWSL経由で編集
@@ -371,6 +608,88 @@ git config --global alias.lg "log --graph --oneline --all"
- **問題**: HTTPSでのpush時に認証失敗
- **対策**: 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
```
**解決方法1SSHに切り替える推奨**
```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アプリの履歴管理
@@ -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 ショートカット集
| 操作 | ショートカット |
@@ -433,12 +780,73 @@ wsl --list --verbose
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に接続できない
1. WSL拡張機能の再インストール
2. WSLの再起動: `wsl --shutdown`
3. VSCodeの再起動
**💡 詳細な解決手順:**
**ステップ1拡張機能の確認**
```bash
# VSCodeで拡張機能を確認
# Ctrl+Shift+X で拡張機能パネルを開く
# "WSL" を検索してインストール済みか確認
```
**ステップ2WSLの再起動**
```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**の理解と実践
@@ -449,15 +857,138 @@ cd ~/projects # Windows側ではなくWSL側で作業
## 推奨リソース
### 📚 公式ドキュメント
- [Pro Git Book日本語](https://git-scm.com/book/ja/v2)
- [Gitea公式ドキュメント](https://docs.gitea.io/)
- [VSCode公式ドキュメント - WSL](https://code.visualstudio.com/docs/remote/wsl)
- [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統合機能を活用しながら、コマンドラインでの操作も習得することで、柔軟な開発スタイルを身につけることができます。
**大切なのは:**
- 🔄 失敗を恐れず、何度も試す
- 📝 学んだことをメモする
- 🤝 わからないことは質問する
- 🎯 小さな目標から始める
---
このカリキュラムは、TODOアプリ開発カリキュラムと連携して使用することを想定しています。

View 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>
);
}

View File

@@ -1,6 +1,8 @@
"use client";
'use client';
import { useState } from "react";
import { useState, useEffect, useCallback, useMemo } from 'react';
import TodoList from '@/components/TodoList';
import FilterButtons from '@/components/FilterButtons';
// Todo型の定義
type Todo = {
@@ -9,107 +11,441 @@ type Todo = {
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() {
// 状態管理の準備(サンプルデータ付き)
const [todos, setTodos] = useState<Todo[]>([
{ id: 1, text: "TypeScriptを学ぶ", completed: false },
{ id: 2, text: "Reactの基礎を理解する", completed: true },
{ id: 3, text: "TODOアプリを作る", completed: false },
]);
// Local Storage のキー
const STORAGE_KEY = 'todo-app-data';
// 入力中のテキストを管理
const [inputText, setInputText] = useState("");
// 状態管理
const [todos, setTodos] = useState<Todo[]>([]);
const [inputValue, setInputValue] = useState<string>('');
const [showConfirm, setShowConfirm] = useState(false);
const [targetId, setTargetId] = useState<number | null>(null);
const [showInputAlert, setShowInputAlert] = useState(false);
const [editingId, setEditingId] = useState<number | null>(null);
const [editText, setEditText] = useState('');
const [filter, setFilter] = useState<FilterType>('all');
// エラー状態の管理
const [error, setError] = useState<AppError | null>(null);
const [showErrorDialog, setShowErrorDialog] = useState(false);
// TODO追加機能
const handleAddTodo = (e: React.FormEvent) => {
e.preventDefault(); // フォームのデフォルト動作を防ぐ
if (inputText.trim() === "") {
return; // 空文字の場合は追加しない
// フィルターに応じたTodoの数をメモ化
const { activeCount, completedCount } = useMemo(() => ({
activeCount: todos.filter(todo => !todo.completed).length,
completedCount: todos.filter(todo => todo.completed).length,
}), [todos]);
// エラー処理用のヘルパー関数をメモ化
const handleError = useCallback((error: AppError) => {
console.error('アプリケーションエラー:', error);
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]);
const newTodo: Todo = {
id: Date.now(), // 簡易的なID生成
text: inputText,
completed: false,
// 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]);
setTodos([...todos, newTodo]); // 新しいTODOを配列に追加
setInputText(""); // 入力欄をクリア
};
// 型安全な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完了状態の切り替え機能
const handleToggleTodo = (id: number) => {
setTodos(
todos.map((todo) =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo
)
);
};
// TODOが変更されるたびに保存(デバウンス処理付き)
useEffect(() => {
const timeoutId = setTimeout(() => {
safeLocalStorageSet(STORAGE_KEY, JSON.stringify(todos));
}, 300); // 300ms後に保存
// TODO削除機能
const handleDeleteTodo = (id: number) => {
setTodos(todos.filter((todo) => todo.id !== id));
};
return () => clearTimeout(timeoutId);
}, [todos, safeLocalStorageSet]);
return (
<main className="min-h-screen p-8 bg-gray-50 dark:bg-gray-900">
<div className="max-w-4xl mx-auto">
<h1 className="text-4xl font-bold text-center text-gray-800 dark:text-gray-100 mb-8">
TODOアプリ
</h1>
{/* TODO入力フォーム */}
<div className="mb-8">
<form onSubmit={handleAddTodo} className="flex gap-2">
<input
type="text"
value={inputText}
onChange={(e) => setInputText(e.target.value)}
placeholder="新しいTODOを入力..."
className="flex-1 px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 dark:bg-gray-800 dark:border-gray-600 dark:text-gray-100"
/>
<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
type="submit"
className="px-6 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600 transition-colors"
onClick={exportData}
className="text-blue-500 hover:text-blue-700 px-3 py-1 rounded transition-colors text-sm"
>
</button>
</form>
<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>
{/* TODOリスト表示エリア */}
<div className="space-y-2">
{todos.map((todo) => (
<div
key={todo.id}
className="flex items-center gap-3 p-4 bg-white dark:bg-gray-800 rounded-lg shadow-sm"
>
<input
type="checkbox"
checked={todo.completed}
onChange={() => handleToggleTodo(todo.id)}
className="w-5 h-5 text-blue-500 rounded focus:ring-2 focus:ring-blue-500"
/>
<span
className={`flex-1 ${
todo.completed
? "line-through text-gray-500 dark:text-gray-400"
: "text-gray-800 dark:text-gray-100"
}`}
{/* 入力フォーム */}
<form onSubmit={handleSubmit} className="mb-4">
<input
type="text"
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
placeholder="TODOを入力してEnterキー"
className="w-full p-3 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
maxLength={100}
/>
<button
type="submit"
className="w-full mt-2 bg-blue-500 text-white p-3 rounded-lg hover:bg-blue-600 transition-colors"
>
</button>
</form>
{/* フィルターボタン */}
<FilterButtons
filter={filter}
totalCount={todos.length}
activeCount={activeCount}
completedCount={completedCount}
onFilterChange={setFilter}
onClearCompleted={clearCompleted}
/>
{/* TODOリスト */}
<TodoList
todos={todos}
filter={filter}
editingId={editingId}
editText={editText}
onToggleTodo={toggleTodo}
onStartEdit={startEdit}
onSaveEdit={saveEdit}
onCancelEdit={cancelEdit}
onDeleteClick={handleDeleteClick}
onEditTextChange={setEditText}
/>
{/* 確認ダイアログ */}
{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" }}
>
{todo.text}
</span>
<button
onClick={() => handleDeleteTodo(todo.id)}
className="px-3 py-1 text-red-500 hover:bg-red-50 dark:hover:bg-red-900/20 rounded transition-colors"
>
</button>
</div>
))}
</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>
);
}
}

View 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
View 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;

View 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