Skip to content

Commit 3c5559f

Browse files
authored
Merge pull request #29 from fun-developers-hub/uiro/demo-implement
tips: フェーズ別の新概念を学ぶタブ切り替えページを追加
2 parents b8416ae + c02dd74 commit 3c5559f

25 files changed

Lines changed: 1316 additions & 3 deletions

File tree

CLAUDE.md

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
# CLAUDE.md
2+
3+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4+
5+
## Project
6+
7+
"TriGonFight" (janken-v2) — a rock-paper-scissors (じゃんけん) web app built with Next.js App Router, deployed to Cloudflare via OpenNext.
8+
9+
## Commands
10+
11+
Use `pnpm` directly (lockfile is `pnpm-lock.yaml`, `packageManager` is pinned to `pnpm@11.3.0`).
12+
13+
```bash
14+
pnpm dev # start dev server
15+
pnpm build # production build (also type-checks)
16+
pnpm lint # eslint .
17+
pnpm lint:fix # eslint . --fix
18+
pnpm format # prettier --write .
19+
pnpm format:check # prettier --check . (CI-enforced)
20+
pnpm preview # opennextjs-cloudflare build && preview (local Cloudflare Worker preview)
21+
pnpm deploy # opennextjs-cloudflare build && deploy (Cloudflare deploy)
22+
```
23+
24+
There is no test suite in this repo (no unit/e2e test runner configured). CI (`.github/workflows/ci.yml`) runs, in order: `pnpm install --frozen-lockfile`, `pnpm lint`, `pnpm format:check`, `pnpm build`. Type checking happens as part of `pnpm build` (`next build`) — do not run `tsc` directly, as noted in global instructions.
25+
26+
## Architecture
27+
28+
- Next.js App Router under `src/app`, path alias `@/*``src/*`.
29+
- Deployment target is Cloudflare Workers via `@opennextjs/cloudflare`. `wrangler.jsonc` points `main` at `.open-next/worker.js` and serves static assets from `.open-next/assets`; `open-next.config.ts` holds the OpenNext build config. `.open-next/` and `.wrangler/` are build artifacts (ignored by eslint) — never hand-edit them.
30+
- Styling is Tailwind CSS v4 via the `@tailwindcss/postcss` plugin, configured in `src/app/globals.css` using `@theme`/`@theme inline` blocks (no separate `tailwind.config`). Custom theme colors (e.g. `newblue`, `newgreen`) are defined there — reuse them instead of introducing new ad-hoc colors. `prettier-plugin-tailwindcss` auto-sorts class names, and `arrowParens: "avoid"` / `bracketSameLine: true` are enforced by Prettier — run `pnpm format` rather than hand-formatting.
31+
- The root route (`src/app/page.tsx`) is the actual game UI (rock/scissors/paper choices, CPU vs. player panels).
32+
- `src/app/tips/` is a separate, self-contained documentation area explaining Next.js/React concepts as they're introduced through the game's development phases — it is not part of the game itself:
33+
- `tips/_data/concepts.ts` is the single source of truth for the tips index: each entry has a `slug`, `title`, `phaseLabel` (which development phase it maps to), optional `issueUrl` (linking to the GitHub issue that introduced the concept), and `summary`.
34+
- Each concept lives in `tips/<slug>/` with three files following the same pattern: `page.tsx` (renders a `ConceptSection` with a Japanese `explanation` and a `demo`), `code.ts` (exports a `snippet` string of example code shown via `CodeBlock`), and `Demo.tsx` (a live, interactive Client Component demonstrating the concept).
35+
- `tips/_components/ConceptSection.tsx` and `tips/_components/CodeBlock.tsx` are the shared layout/rendering primitives for every concept page; `CodeBlock` renders syntax-highlighted code server-side via `shiki` (`codeToHtml`, theme `github-dark`).
36+
- When adding a new tips entry: add its metadata to `concepts.ts`, then create the three files above following an existing slug (e.g. `use-state-basics`) as the template.
37+
- Explanatory text throughout the tips section and UI-facing copy is written in Japanese; match that when adding to these areas.

package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,11 @@
1616
},
1717
"dependencies": {
1818
"@opennextjs/cloudflare": "^1.19.11",
19+
"@tanstack/react-query": "^5.101.4",
1920
"next": "16.2.4",
2021
"react": "19.2.4",
21-
"react-dom": "19.2.4"
22+
"react-dom": "19.2.4",
23+
"shiki": "^4.3.1"
2224
},
2325
"devDependencies": {
2426
"@tailwindcss/postcss": "^4.3.1",

pnpm-lock.yaml

Lines changed: 355 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/app/page.tsx

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
"use client";
22
import Image from "next/image";
3-
3+
import Link from "next/link";
44
import { useState } from "react";
55

66
export default function Page() {
@@ -14,10 +14,15 @@ export default function Page() {
1414
};
1515
return (
1616
<div className="flex h-full flex-col">
17-
<header>
17+
<header className="relative">
1818
<h1 className="bg-newgreen text-center text-4xl">TriGonFight</h1>
1919
{/* bgはbackground */}
2020
{/* この環境でh1は意味がないけど、タイトルというマークのためにつけてる */}
21+
<Link
22+
href="/tips"
23+
className="absolute top-1/2 right-2 -translate-y-1/2 text-sm underline">
24+
Tips
25+
</Link>
2126
</header>
2227
<div className="relative flex flex-1 items-center justify-center bg-violet-950 text-white">
2328
<div className="size-35 rounded-full bg-pink-700"></div>
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { getSingletonHighlighter } from "shiki";
2+
import { createJavaScriptRegexEngine } from "shiki/engine/javascript";
3+
4+
type CodeBlockProps = {
5+
code: string;
6+
lang?: "tsx" | "ts" | "css";
7+
};
8+
9+
export async function CodeBlock({ code, lang = "tsx" }: CodeBlockProps) {
10+
const highlighter = await getSingletonHighlighter({
11+
langs: [lang],
12+
themes: ["github-dark"],
13+
engine: createJavaScriptRegexEngine(),
14+
});
15+
const html = highlighter.codeToHtml(code, { lang, theme: "github-dark" });
16+
17+
return (
18+
<div
19+
className="overflow-x-auto rounded-md text-sm [&_pre]:m-0 [&_pre]:p-4 [&_pre]:font-mono"
20+
dangerouslySetInnerHTML={{ __html: html }}
21+
/>
22+
);
23+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import Link from "next/link";
2+
import type { ReactNode } from "react";
3+
4+
type ConceptSectionProps = {
5+
title: string;
6+
explanation: ReactNode;
7+
demo: ReactNode;
8+
};
9+
10+
export function ConceptSection({
11+
title,
12+
explanation,
13+
demo,
14+
}: ConceptSectionProps) {
15+
return (
16+
<article>
17+
<Link href="/tips" className="text-newblue mb-4 inline-block underline">
18+
← Tips一覧へ戻る
19+
</Link>
20+
<h2 className="mb-4 text-2xl font-bold">{title}</h2>
21+
<section className="mb-8">
22+
<h3 className="mb-2 text-lg font-bold">説明</h3>
23+
<div className="flex flex-col gap-3 text-sm leading-relaxed">
24+
{explanation}
25+
</div>
26+
</section>
27+
<section>
28+
<h3 className="mb-2 text-lg font-bold">実装例</h3>
29+
<div className="flex flex-col gap-4">{demo}</div>
30+
</section>
31+
</article>
32+
);
33+
}

src/app/tips/_data/concepts.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
export type Concept = {
2+
slug: string;
3+
title: string;
4+
phaseLabel: string;
5+
issueUrl?: string;
6+
summary: string;
7+
};
8+
9+
export const concepts: Concept[] = [
10+
{
11+
slug: "use-state-basics",
12+
title: "Client Components と useState",
13+
phaseLabel: "Phase 1",
14+
summary:
15+
"「use client」とuseStateによるUI状態管理・イベントハンドリングの基礎。",
16+
},
17+
{
18+
slug: "next-font-optimization",
19+
title: "next/font でのフォント設定",
20+
phaseLabel: "Phase 1-4",
21+
issueUrl:
22+
"https://github.com/fun-developers-hub/janken-v2-frontend/issues/23",
23+
summary:
24+
"next/font/google でフォントを読み込み、実際に適用するまでの設定方法。",
25+
},
26+
{
27+
slug: "client-side-fetch",
28+
title: "fetch によるクライアントサイド取得",
29+
phaseLabel: "Phase 2",
30+
issueUrl:
31+
"https://github.com/fun-developers-hub/janken-v2-frontend/issues/11",
32+
summary:
33+
"loading/error状態を含む、クライアントコンポーネントでのfetchパターン。",
34+
},
35+
{
36+
slug: "tanstack-query-mutation",
37+
title: "TanStack Query の useMutation で書き換える",
38+
phaseLabel: "Phase 2(発展)",
39+
summary:
40+
"自前のuseState/try-catchによる状態管理を、useMutationに任せる書き方。",
41+
},
42+
{
43+
slug: "count-state-management",
44+
title: "回数カウンターの状態管理",
45+
phaseLabel: "Phase 3(任意)",
46+
issueUrl:
47+
"https://github.com/fun-developers-hub/janken-v2-frontend/issues/18",
48+
summary: "useReducerを使った、複数アクションを持つ状態のカウント管理。",
49+
},
50+
];
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
"use client";
2+
3+
import { useJankenFetch } from "./useJankenFetch";
4+
5+
export function Demo() {
6+
const { status, cpuHand, result, errorMessage, fetchHand } = useJankenFetch();
7+
8+
return (
9+
<div className="flex flex-col items-center gap-4">
10+
<div className="flex gap-2">
11+
<button
12+
type="button"
13+
onClick={() => fetchHand("rock")}
14+
className="bg-newblue rounded-md px-4 py-2 text-white">
15+
正常なリクエスト(グー)
16+
</button>
17+
<button
18+
type="button"
19+
onClick={() => fetchHand("invalid")}
20+
className="rounded-md bg-red-600 px-4 py-2 text-white">
21+
エラーを発生させる
22+
</button>
23+
</div>
24+
<div className="text-sm">
25+
状態: <span className="font-bold">{status}</span>
26+
{status === "success" ? (
27+
<div className="mt-1">
28+
CPUの手: {cpuHand} / 結果: {result}
29+
</div>
30+
) : null}
31+
{status === "error" ? <div className="mt-1">{errorMessage}</div> : null}
32+
</div>
33+
</div>
34+
);
35+
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
export const hookSnippet = `// useJankenFetch.ts
2+
"use client";
3+
4+
import { useState } from "react";
5+
6+
type Status = "idle" | "loading" | "success" | "error";
7+
8+
export function useJankenFetch() {
9+
const [status, setStatus] = useState<Status>("idle");
10+
const [cpuHand, setCpuHand] = useState("");
11+
const [result, setResult] = useState("");
12+
const [errorMessage, setErrorMessage] = useState("");
13+
14+
async function fetchHand(userHand: string) {
15+
setStatus("loading");
16+
try {
17+
const res = await fetch("https://janken.ma41.net/janken", {
18+
method: "POST",
19+
headers: { "Content-Type": "application/json" },
20+
body: JSON.stringify({ user_hand: userHand }),
21+
});
22+
const data = await res.json();
23+
if (!res.ok) {
24+
throw new Error(data.error ?? "リクエストに失敗しました");
25+
}
26+
setCpuHand(data.cpu_hand);
27+
setResult(data.result);
28+
setStatus("success");
29+
} catch (error) {
30+
setErrorMessage(error instanceof Error ? error.message : "不明なエラー");
31+
setStatus("error");
32+
}
33+
}
34+
35+
return { status, cpuHand, result, errorMessage, fetchHand };
36+
}
37+
`;
38+
39+
export const demoSnippet = `// Demo.tsx
40+
"use client";
41+
42+
import { useJankenFetch } from "./useJankenFetch";
43+
44+
export function Demo() {
45+
const { status, cpuHand, result, fetchHand } = useJankenFetch();
46+
47+
return (
48+
<div>
49+
{/* cpuHand/result はフック側で個別の状態として管理し、
50+
Demo は「呼ぶ」「表示する」だけになる */}
51+
<button onClick={() => fetchHand("rock")}>グー</button>
52+
</div>
53+
);
54+
}
55+
`;
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import { CodeBlock } from "../_components/CodeBlock";
2+
import { ConceptSection } from "../_components/ConceptSection";
3+
import { demoSnippet, hookSnippet } from "./code";
4+
import { Demo } from "./Demo";
5+
6+
export default function Page() {
7+
return (
8+
<ConceptSection
9+
title="fetch によるクライアントサイド取得"
10+
explanation={
11+
<>
12+
<p>
13+
実バックエンド(<code>https://janken.ma41.net</code>)が
14+
どんなAPIを持っているかは、
15+
<a
16+
href="https://janken.ma41.net/swagger/index.html"
17+
className="text-newblue underline">
18+
Swagger UI
19+
</a>
20+
を眺めてみると分かる。パスやリクエスト/レスポンスの形が
21+
一覧できるので、fetchを書く前に一度見ておくとよい。
22+
</p>
23+
<p>
24+
Client Component からAPIを呼び出すときは、 「今どういう状態か」を{" "}
25+
<code>idle</code>/<code>loading</code>/<code>success</code>/
26+
<code>error</code> のような文字列で保持しておくと、
27+
読み込み中の表示やエラー表示を素直に出し分けられる。
28+
</p>
29+
<p>
30+
<code>try/catch/finally</code> を使い、リクエスト開始時に{" "}
31+
<code>loading</code>
32+
へ、成功したら <code>success</code>
33+
へ、失敗したら <code>error</code>{" "}
34+
へ状態を遷移させる。レスポンスが200番台でなくても <code>fetch</code>
35+
自体は例外を投げないため、
36+
<code>res.ok</code> を確認して自分でエラーとして扱う必要がある。
37+
</p>
38+
<p>
39+
下のデモでは、実バックエンド(
40+
<code>https://janken.ma41.net</code>)の <code>POST /janken</code>{" "}
41+
を直接呼び出し、正常なリクエストとエラーになるリクエストの
42+
両方を試せる。
43+
</p>
44+
<p>
45+
<code>status</code>/<code>cpuHand</code>/<code>result</code>{" "}
46+
の管理とfetch処理は、
47+
<code>useJankenFetch</code>{" "}
48+
というカスタムフックに抽出している。こうすると <code>Demo</code>{" "}
49+
コンポーネント側は「フックを呼んで、
50+
返ってきた値を表示する」だけになり、見通しがよくなる。
51+
</p>
52+
</>
53+
}
54+
demo={
55+
<>
56+
<CodeBlock code={hookSnippet} />
57+
<CodeBlock code={demoSnippet} />
58+
<Demo />
59+
</>
60+
}
61+
/>
62+
);
63+
}

0 commit comments

Comments
 (0)