├── .gitignore
├── README.md
├── package-lock.json
├── package.json
└── packages
├── training-api
├── .eslintrc.json
├── .gitignore
├── README.md
├── next.config.mjs
├── package.json
├── src
│ ├── _mock
│ │ ├── categories.ts
│ │ ├── index.ts
│ │ └── photos.ts
│ └── app
│ │ └── api
│ │ ├── categories
│ │ ├── [categoryName]
│ │ │ └── route.ts
│ │ ├── id
│ │ │ └── [categoryId]
│ │ │ │ └── route.ts
│ │ └── route.ts
│ │ └── photos
│ │ ├── [photoId]
│ │ └── route.ts
│ │ └── route.ts
└── tsconfig.json
├── training-web-1
├── .eslintrc.json
├── .gitignore
├── README.md
├── next.config.mjs
├── package.json
├── src
│ ├── app
│ │ ├── _components
│ │ │ ├── Footer
│ │ │ │ ├── index.tsx
│ │ │ │ └── style.module.css
│ │ │ ├── Header
│ │ │ │ ├── index.tsx
│ │ │ │ └── style.module.css
│ │ │ └── Nav
│ │ │ │ ├── index.tsx
│ │ │ │ └── style.module.css
│ │ ├── categories
│ │ │ ├── [categoryName]
│ │ │ │ ├── page.module.css
│ │ │ │ └── page.tsx
│ │ │ └── page.tsx
│ │ ├── company-info
│ │ │ └── page.tsx
│ │ ├── layout.module.css
│ │ ├── layout.tsx
│ │ ├── page.tsx
│ │ ├── photos
│ │ │ └── [photoId]
│ │ │ │ └── page.tsx
│ │ └── privacy-policy
│ │ │ └── page.tsx
│ └── styles
│ │ └── globals.css
└── tsconfig.json
├── training-web-2
├── .eslintrc.json
├── .gitignore
├── README.md
├── next.config.mjs
├── package.json
├── src
│ ├── app
│ │ ├── _components
│ │ │ ├── Footer
│ │ │ │ ├── index.tsx
│ │ │ │ └── style.module.css
│ │ │ ├── Header
│ │ │ │ ├── index.tsx
│ │ │ │ └── style.module.css
│ │ │ └── Nav
│ │ │ │ ├── IconButton.tsx
│ │ │ │ ├── index.tsx
│ │ │ │ └── style.module.css
│ │ ├── categories
│ │ │ ├── [categoryName]
│ │ │ │ ├── page.module.css
│ │ │ │ └── page.tsx
│ │ │ ├── layout.module.css
│ │ │ ├── layout.tsx
│ │ │ └── page.tsx
│ │ ├── company-info
│ │ │ └── page.tsx
│ │ ├── layout.module.css
│ │ ├── layout.tsx
│ │ ├── page.module.css
│ │ ├── page.tsx
│ │ ├── photos
│ │ │ ├── [photoId]
│ │ │ │ ├── LikeButton.tsx
│ │ │ │ ├── page.module.css
│ │ │ │ └── page.tsx
│ │ │ ├── layout.module.css
│ │ │ └── layout.tsx
│ │ └── privacy-policy
│ │ │ └── page.tsx
│ ├── constants.ts
│ ├── styles
│ │ └── globals.css
│ └── utils
│ │ └── index.ts
└── tsconfig.json
├── training-web-3
├── .eslintrc.json
├── .gitignore
├── README.md
├── next.config.mjs
├── package.json
├── src
│ ├── app
│ │ ├── _components
│ │ │ ├── Footer
│ │ │ │ ├── index.tsx
│ │ │ │ └── style.module.css
│ │ │ ├── Header
│ │ │ │ ├── index.tsx
│ │ │ │ └── style.module.css
│ │ │ └── Nav
│ │ │ │ ├── index.tsx
│ │ │ │ └── style.module.css
│ │ ├── categories
│ │ │ ├── [categoryName]
│ │ │ │ ├── page.module.css
│ │ │ │ └── page.tsx
│ │ │ ├── layout.module.css
│ │ │ ├── layout.tsx
│ │ │ └── page.tsx
│ │ ├── company-info
│ │ │ └── page.tsx
│ │ ├── layout.module.css
│ │ ├── layout.tsx
│ │ ├── page.module.css
│ │ ├── page.tsx
│ │ ├── photos
│ │ │ ├── [photoId]
│ │ │ │ ├── LikeButton.tsx
│ │ │ │ ├── page.module.css
│ │ │ │ └── page.tsx
│ │ │ ├── layout.module.css
│ │ │ └── layout.tsx
│ │ └── privacy-policy
│ │ │ └── page.tsx
│ ├── constants.ts
│ ├── styles
│ │ └── globals.css
│ ├── type.ts
│ └── utils
│ │ └── index.ts
└── tsconfig.json
└── training-web-4
├── .eslintrc.json
├── .gitignore
├── README.md
├── next.config.mjs
├── package.json
├── src
├── app
│ ├── (site)
│ │ ├── _components
│ │ │ └── Nav
│ │ │ │ ├── index.tsx
│ │ │ │ └── style.module.css
│ │ ├── categories
│ │ │ ├── [categoryName]
│ │ │ │ ├── page.module.css
│ │ │ │ └── page.tsx
│ │ │ ├── layout.module.css
│ │ │ ├── layout.tsx
│ │ │ ├── loading.tsx
│ │ │ ├── not-found.tsx
│ │ │ └── page.tsx
│ │ ├── layout.module.css
│ │ ├── layout.tsx
│ │ ├── page.module.css
│ │ ├── page.tsx
│ │ └── photos
│ │ │ ├── [photoId]
│ │ │ ├── LikeButton.tsx
│ │ │ ├── page.module.css
│ │ │ └── page.tsx
│ │ │ ├── layout.module.css
│ │ │ ├── layout.tsx
│ │ │ ├── loading.tsx
│ │ │ └── not-found.tsx
│ ├── (static)
│ │ ├── company-info
│ │ │ └── page.tsx
│ │ ├── layout.module.css
│ │ ├── layout.tsx
│ │ └── privacy-policy
│ │ │ └── page.tsx
│ ├── _components
│ │ ├── Footer
│ │ │ ├── index.tsx
│ │ │ └── style.module.css
│ │ └── Header
│ │ │ ├── index.tsx
│ │ │ └── style.module.css
│ ├── api
│ │ └── photos
│ │ │ └── [photoId]
│ │ │ └── like
│ │ │ └── route.ts
│ ├── favicon.ico
│ ├── global-error.tsx
│ └── layout.tsx
├── constants.ts
├── styles
│ └── globals.css
├── type.ts
└── utils
│ └── index.ts
└── tsconfig.json
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | .minio
3 | .pdf
4 | .vscode
5 | *.psd
6 | tsconfig.tsbuildinfo
7 | node_modules
8 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # practical-nextjs-book/training
2 |
3 | App Router の概要を理解するための、練習用サンプルコードです。各 package は、書籍構成に応じて段階的に機能強化されています。
4 |
5 | ## 練習用サンプルコード
6 |
7 | - training-web-1: 1章
8 | - training-web-2: 1章〜2章
9 | - training-web-3: 2章
10 | - training-web-4: 3章〜4章
11 |
12 | ## 練習用サンプルコード APIサーバー
13 |
14 | - training-api: 2章〜4章
15 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "training",
3 | "private": true,
4 | "engines": {
5 | "node": ">=18.17.0"
6 | },
7 | "workspaces": [
8 | "packages/*"
9 | ],
10 | "devDependencies": {
11 | "npm-run-all": "^4.1.5",
12 | "rimraf": "^5.0.5"
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/packages/training-api/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "parser": "@typescript-eslint/parser",
3 | "parserOptions": {
4 | "sourceType": "module",
5 | "project": "./tsconfig.json"
6 | },
7 | "plugins": [
8 | "@typescript-eslint",
9 | "unused-imports"
10 | ],
11 | "extends": [
12 | "next/core-web-vitals",
13 | "plugin:@typescript-eslint/recommended",
14 | "plugin:import/recommended",
15 | "prettier"
16 | ],
17 | "rules": {
18 | "import/order": [
19 | "error",
20 | {
21 | "groups": [
22 | "builtin",
23 | "external",
24 | "parent",
25 | "sibling",
26 | "index",
27 | "object",
28 | "type"
29 | ],
30 | "pathGroups": [
31 | {
32 | "pattern": "react",
33 | "group": "builtin",
34 | "position": "before"
35 | },
36 | {
37 | "pattern": "@/**",
38 | "group": "parent",
39 | "position": "before"
40 | }
41 | ],
42 | "pathGroupsExcludedImportTypes": [
43 | "builtin"
44 | ],
45 | "alphabetize": {
46 | "order": "asc"
47 | },
48 | "newlines-between": "never"
49 | }
50 | ],
51 | "@typescript-eslint/consistent-type-imports": "warn",
52 | "unused-imports/no-unused-imports": "error"
53 | }
54 | }
--------------------------------------------------------------------------------
/packages/training-api/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # next.js
12 | /.next/
13 | /out/
14 |
15 | # production
16 | /build
17 |
18 | # misc
19 | .DS_Store
20 | *.pem
21 |
22 | # debug
23 | npm-debug.log*
24 | yarn-debug.log*
25 | yarn-error.log*
26 |
27 | # local env files
28 | .env*.local
29 |
30 | # vercel
31 | .vercel
32 |
33 | # typescript
34 | *.tsbuildinfo
35 | next-env.d.ts
36 |
--------------------------------------------------------------------------------
/packages/training-api/README.md:
--------------------------------------------------------------------------------
1 | # training/training-api
2 |
3 | App Router 練習用プロジェクト
4 |
5 | - 2-2.Server Component のデータ取得
6 | - 2-3.動的データ取得・静的データ取得
7 | - 2-4.Route のレンダリング
8 | - 4.2:Route Handlerのレンダリング
9 |
--------------------------------------------------------------------------------
/packages/training-api/next.config.mjs:
--------------------------------------------------------------------------------
1 | /** @type {import('next').NextConfig} */
2 | const nextConfig = {
3 | }
4 |
5 | export default nextConfig;
6 |
--------------------------------------------------------------------------------
/packages/training-api/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "training-api",
3 | "version": "0.1.0",
4 | "private": true,
5 | "description": "App Router 練習アプリ用 APIサーバー(第1部8章〜11章)",
6 | "author": "takepepe",
7 | "license": "MIT",
8 | "scripts": {
9 | "dev": "next dev -p 8080",
10 | "build": "next build",
11 | "start": "next start -p 8080",
12 | "clean": "rimraf .next/cache/fetch-cache",
13 | "clean-dev": "run-s clean dev",
14 | "clean-build": "run-s clean build",
15 | "clean-start": "run-s clean build start",
16 | "lint": "next lint",
17 | "typecheck": "tsc --noEmit",
18 | "format": "run-s format:*",
19 | "format:elint": "eslint --fix './src/**/*.{js,ts,tsx}'",
20 | "format:prettier": "prettier --write './src/**/*.{js,ts,tsx}'"
21 | },
22 | "dependencies": {
23 | "next": "14.2.2",
24 | "react": "^18",
25 | "react-dom": "^18"
26 | },
27 | "devDependencies": {
28 | "@types/node": "^20",
29 | "@types/react": "^18",
30 | "@types/react-dom": "^18",
31 | "@typescript-eslint/eslint-plugin": "^6.9.0",
32 | "@typescript-eslint/parser": "^6.9.0",
33 | "eslint": "^8",
34 | "eslint-config-next": "14.2.2",
35 | "eslint-config-prettier": "^8.8.0",
36 | "eslint-plugin-unused-imports": "^3.0.0",
37 | "prettier": "^3.0.0",
38 | "ts-node": "^10.9.1",
39 | "typescript": "^5.2.2"
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/packages/training-api/src/_mock/categories.ts:
--------------------------------------------------------------------------------
1 | import type { Category } from ".";
2 |
3 | export const categories: Category[] = [
4 | {
5 | id: "1",
6 | name: "flower",
7 | label: "花",
8 | description: "description",
9 | imageUrl: "/images/no-image.jpg",
10 | },
11 | {
12 | id: "2",
13 | name: "animal",
14 | label: "動物",
15 | description: "description",
16 | imageUrl: "/images/no-image.jpg",
17 | },
18 | {
19 | id: "3",
20 | name: "landscape",
21 | label: "風景",
22 | description: "description",
23 | imageUrl: "/images/no-image.jpg",
24 | },
25 | ];
26 |
--------------------------------------------------------------------------------
/packages/training-api/src/_mock/index.ts:
--------------------------------------------------------------------------------
1 | export type Photo = {
2 | id: string;
3 | title: string;
4 | description: string;
5 | imageUrl: string;
6 | authorId: string;
7 | categoryId: string;
8 | };
9 |
10 | export type Category = {
11 | id: string;
12 | name: string;
13 | label: string;
14 | description: string;
15 | imageUrl: string;
16 | };
17 |
--------------------------------------------------------------------------------
/packages/training-api/src/_mock/photos.ts:
--------------------------------------------------------------------------------
1 | import type { Photo } from ".";
2 |
3 | export const photos: Photo[] = [...new Array(10)].map((_, i) => {
4 | const id = `00${i + 1}`.slice(-3);
5 | const authorId = `${(i % 3) + 1}`;
6 | return {
7 | id,
8 | title: `Test-${id}`,
9 | description: `Test-${id} description...`.repeat(10),
10 | imageUrl: "/images/no-image.jpg",
11 | authorId,
12 | categoryId: `${(i % 3) + 1}`,
13 | };
14 | });
15 |
--------------------------------------------------------------------------------
/packages/training-api/src/app/api/categories/[categoryName]/route.ts:
--------------------------------------------------------------------------------
1 | import { type NextRequest } from "next/server";
2 | import { categories } from "@/_mock/categories";
3 |
4 | export async function GET(
5 | _: NextRequest,
6 | { params }: { params: { categoryName: string } },
7 | ) {
8 | // 🚧: DBに接続しレコードを取得する
9 | const category = categories.find(
10 | (category) => category.name === params.categoryName,
11 | );
12 | if (!category) {
13 | return Response.json({ message: "Not Found" }, { status: 404 });
14 | }
15 | return Response.json({ category });
16 | }
17 |
--------------------------------------------------------------------------------
/packages/training-api/src/app/api/categories/id/[categoryId]/route.ts:
--------------------------------------------------------------------------------
1 | import { type NextRequest } from "next/server";
2 | import { categories } from "@/_mock/categories";
3 |
4 | export async function GET(
5 | _: NextRequest,
6 | { params }: { params: { categoryId: string } },
7 | ) {
8 | // 🚧: DBに接続しレコードを取得する
9 | const category = categories.find(
10 | (category) => category.id === params.categoryId,
11 | );
12 | if (!category) {
13 | return Response.json({ message: "Not Found" }, { status: 404 });
14 | }
15 | return Response.json({ category });
16 | }
17 |
--------------------------------------------------------------------------------
/packages/training-api/src/app/api/categories/route.ts:
--------------------------------------------------------------------------------
1 | import { categories } from "@/_mock/categories";
2 |
3 | export async function GET() {
4 | // 🚧: DBに接続しレコードを取得する
5 | return Response.json({ categories });
6 | }
7 |
--------------------------------------------------------------------------------
/packages/training-api/src/app/api/photos/[photoId]/route.ts:
--------------------------------------------------------------------------------
1 | import { type NextRequest } from "next/server";
2 | import { photos } from "@/_mock/photos";
3 |
4 | export async function GET(
5 | _: NextRequest,
6 | { params }: { params: { photoId: string } },
7 | ) {
8 | // 🚧: DBに接続しレコードを取得する
9 | const photo = photos.find((photo) => photo.id === params.photoId);
10 | if (!photo) {
11 | return Response.json({ message: "Not Found" }, { status: 404 });
12 | }
13 | return Response.json({ photo });
14 | }
15 |
--------------------------------------------------------------------------------
/packages/training-api/src/app/api/photos/route.ts:
--------------------------------------------------------------------------------
1 | import { photos } from "@/_mock/photos";
2 |
3 | export async function GET() {
4 | console.log("GET /api/photos");
5 | // 🚧: DBに接続しレコードを取得する
6 | return Response.json({ photos });
7 | }
8 |
9 | export const dynamic = "force-dynamic";
10 |
--------------------------------------------------------------------------------
/packages/training-api/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "lib": ["dom", "dom.iterable", "esnext"],
5 | "allowJs": true,
6 | "skipLibCheck": true,
7 | "strict": true,
8 | "forceConsistentCasingInFileNames": true,
9 | "noEmit": true,
10 | "esModuleInterop": true,
11 | "module": "esnext",
12 | "moduleResolution": "node",
13 | "resolveJsonModule": true,
14 | "isolatedModules": true,
15 | "jsx": "preserve",
16 | "incremental": true,
17 | "plugins": [
18 | {
19 | "name": "next"
20 | }
21 | ],
22 | "paths": {
23 | "@/*": ["./src/*"]
24 | }
25 | },
26 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
27 | "exclude": ["node_modules"]
28 | }
29 |
--------------------------------------------------------------------------------
/packages/training-web-1/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "parser": "@typescript-eslint/parser",
3 | "parserOptions": {
4 | "sourceType": "module",
5 | "project": "./tsconfig.json"
6 | },
7 | "plugins": [
8 | "@typescript-eslint",
9 | "unused-imports"
10 | ],
11 | "extends": [
12 | "next/core-web-vitals",
13 | "plugin:@typescript-eslint/recommended",
14 | "plugin:import/recommended",
15 | "prettier"
16 | ],
17 | "rules": {
18 | "import/order": [
19 | "error",
20 | {
21 | "groups": [
22 | "builtin",
23 | "external",
24 | "parent",
25 | "sibling",
26 | "index",
27 | "object",
28 | "type"
29 | ],
30 | "pathGroups": [
31 | {
32 | "pattern": "react",
33 | "group": "builtin",
34 | "position": "before"
35 | },
36 | {
37 | "pattern": "@/**",
38 | "group": "parent",
39 | "position": "before"
40 | }
41 | ],
42 | "pathGroupsExcludedImportTypes": [
43 | "builtin"
44 | ],
45 | "alphabetize": {
46 | "order": "asc"
47 | },
48 | "newlines-between": "never"
49 | }
50 | ],
51 | "@typescript-eslint/consistent-type-imports": "warn",
52 | "unused-imports/no-unused-imports": "error"
53 | }
54 | }
--------------------------------------------------------------------------------
/packages/training-web-1/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # next.js
12 | /.next/
13 | /out/
14 |
15 | # production
16 | /build
17 |
18 | # misc
19 | .DS_Store
20 | *.pem
21 |
22 | # debug
23 | npm-debug.log*
24 | yarn-debug.log*
25 | yarn-error.log*
26 |
27 | # local env files
28 | .env*.local
29 |
30 | # vercel
31 | .vercel
32 |
33 | # typescript
34 | *.tsbuildinfo
35 | next-env.d.ts
36 |
--------------------------------------------------------------------------------
/packages/training-web-1/README.md:
--------------------------------------------------------------------------------
1 | # training/training-web-1
2 |
3 | App Router 練習用プロジェクト
4 |
5 | - 1-2.アプリケーションのルーティング
6 | - 1-3.SPA ならではのナビゲーション
7 |
--------------------------------------------------------------------------------
/packages/training-web-1/next.config.mjs:
--------------------------------------------------------------------------------
1 | /** @type {import('next').NextConfig} */
2 | const nextConfig = {
3 | }
4 |
5 | export default nextConfig;
6 |
--------------------------------------------------------------------------------
/packages/training-web-1/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "training-web-1",
3 | "version": "0.1.0",
4 | "private": true,
5 | "description": "App Router 練習アプリ(第1部2章〜3章)",
6 | "author": "takepepe",
7 | "license": "MIT",
8 | "scripts": {
9 | "dev": "next dev",
10 | "build": "next build",
11 | "start": "next start",
12 | "clean": "rimraf .next/cache/fetch-cache",
13 | "clean-dev": "run-s clean dev",
14 | "clean-build": "run-s clean build",
15 | "clean-start": "run-s clean build start",
16 | "lint": "next lint",
17 | "typecheck": "tsc --noEmit",
18 | "format": "run-s format:*",
19 | "format:elint": "eslint --fix './src/**/*.{js,ts,tsx}'",
20 | "format:prettier": "prettier --write './src/**/*.{js,ts,tsx}'"
21 | },
22 | "dependencies": {
23 | "next": "14.2.2",
24 | "react": "^18",
25 | "react-dom": "^18"
26 | },
27 | "devDependencies": {
28 | "@types/node": "^20",
29 | "@types/react": "^18",
30 | "@types/react-dom": "^18",
31 | "@typescript-eslint/eslint-plugin": "^6.9.0",
32 | "@typescript-eslint/parser": "^6.9.0",
33 | "eslint": "^8",
34 | "eslint-config-next": "14.2.2",
35 | "eslint-config-prettier": "^8.8.0",
36 | "eslint-plugin-unused-imports": "^3.0.0",
37 | "prettier": "^3.0.0",
38 | "ts-node": "^10.9.1",
39 | "typescript": "^5.2.2"
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/packages/training-web-1/src/app/_components/Footer/index.tsx:
--------------------------------------------------------------------------------
1 | import Link from "next/link";
2 | import styles from "./style.module.css";
3 |
4 | export function Footer() {
5 | return (
6 |
16 | );
17 | }
18 |
--------------------------------------------------------------------------------
/packages/training-web-1/src/app/_components/Footer/style.module.css:
--------------------------------------------------------------------------------
1 | .footer {
2 | padding: 24px;
3 | border-top: 1px solid #000;
4 | }
--------------------------------------------------------------------------------
/packages/training-web-1/src/app/_components/Header/index.tsx:
--------------------------------------------------------------------------------
1 | import Link from "next/link";
2 | import styles from "./style.module.css";
3 |
4 | export function Header() {
5 | return (
6 |
12 | );
13 | }
14 |
--------------------------------------------------------------------------------
/packages/training-web-1/src/app/_components/Header/style.module.css:
--------------------------------------------------------------------------------
1 | .header {
2 | padding: 24px;
3 | border-bottom: 1px solid #000;
4 | }
--------------------------------------------------------------------------------
/packages/training-web-1/src/app/_components/Nav/index.tsx:
--------------------------------------------------------------------------------
1 | import Link from "next/link";
2 | import styles from "./style.module.css";
3 |
4 | export function Nav() {
5 | return (
6 |
16 | );
17 | }
18 |
--------------------------------------------------------------------------------
/packages/training-web-1/src/app/_components/Nav/style.module.css:
--------------------------------------------------------------------------------
1 | .nav {
2 | min-width: 220px;
3 | padding: 24px;
4 | border-right: 1px solid #000;
5 | }
--------------------------------------------------------------------------------
/packages/training-web-1/src/app/categories/[categoryName]/page.module.css:
--------------------------------------------------------------------------------
1 | .pagination {
2 | border-top: 1px solid #000;
3 | }
--------------------------------------------------------------------------------
/packages/training-web-1/src/app/categories/[categoryName]/page.tsx:
--------------------------------------------------------------------------------
1 | type Props = {
2 | params: { categoryName: string };
3 | searchParams: { [key: string]: string | string[] | undefined };
4 | };
5 |
6 | // ★:props からパスパラメーター、URL 検索パラメーターが参照できる
7 | export default function Page({ params, searchParams }: Props) {
8 | const page = typeof searchParams.page === "string" ? searchParams.page : "1";
9 | return (
10 |
11 |
カテゴリー別一覧画面
12 |
カテゴリー「{params.categoryName}」
13 |
ページ番号:「{page}」
14 |
15 | );
16 | }
17 |
--------------------------------------------------------------------------------
/packages/training-web-1/src/app/categories/page.tsx:
--------------------------------------------------------------------------------
1 | import Link from "next/link";
2 |
3 | export default function Page() {
4 | return (
5 |
6 |
カテゴリー一覧画面
7 |
8 | -
9 | {/* ★:Route の /categories/[categoryName] に遷移する */}
10 | 花
11 |
12 | -
13 | 動物
14 |
15 | -
16 | 風景
17 |
18 |
19 |
20 | );
21 | }
22 |
--------------------------------------------------------------------------------
/packages/training-web-1/src/app/company-info/page.tsx:
--------------------------------------------------------------------------------
1 | export default function Page() {
2 | return (
3 |
4 |
企業概要
5 |
6 | );
7 | }
8 |
--------------------------------------------------------------------------------
/packages/training-web-1/src/app/layout.module.css:
--------------------------------------------------------------------------------
1 | .container {
2 | display: flex;
3 | flex-direction: column;
4 | min-height: 100vh;
5 | background-color: #d8dcea;
6 | }
7 |
8 | .content {
9 | flex-grow: 1;
10 | display: flex;
11 | }
12 |
13 | .main {
14 | flex-grow: 1;
15 | display: flex;
16 | }
--------------------------------------------------------------------------------
/packages/training-web-1/src/app/layout.tsx:
--------------------------------------------------------------------------------
1 | import "../styles/globals.css";
2 | import { Footer } from "./_components/Footer";
3 | import { Header } from "./_components/Header";
4 | import { Nav } from "./_components/Nav";
5 | import styles from "./layout.module.css";
6 |
7 | export default function RootLayout({
8 | children,
9 | }: {
10 | children: React.ReactNode;
11 | }) {
12 | // ★:全ての画面に適用される共通レイアウト
13 | return (
14 |
15 |
16 |
17 |
18 |
19 |
20 | {children}
21 |
22 |
23 |
24 |
25 |
26 | );
27 | }
28 |
--------------------------------------------------------------------------------
/packages/training-web-1/src/app/page.tsx:
--------------------------------------------------------------------------------
1 | import Link from "next/link";
2 |
3 | // ★:Segment の画面を提供するファイル
4 | export default function Page() {
5 | return (
6 |
7 |
トップ画面
8 |
9 | -
10 | {/* ★:Route の /photos/[photoId] に遷移する */}
11 | 写真1
12 |
13 | -
14 | 写真2
15 |
16 | -
17 | 写真3
18 |
19 |
20 |
21 | );
22 | }
23 |
--------------------------------------------------------------------------------
/packages/training-web-1/src/app/photos/[photoId]/page.tsx:
--------------------------------------------------------------------------------
1 | type Props = {
2 | params: { photoId: string };
3 | };
4 |
5 | // ★:props からパスパラメーターが参照できる
6 | export default function Page({ params }: Props) {
7 | return (
8 |
9 |
写真ID「{params.photoId}」の詳細画面
10 |
11 |
12 |
13 | 概要 |
14 | 概要テキスト |
15 |
16 |
17 |
18 |
19 | );
20 | }
21 |
--------------------------------------------------------------------------------
/packages/training-web-1/src/app/privacy-policy/page.tsx:
--------------------------------------------------------------------------------
1 | export default function Page() {
2 | return (
3 |
4 |
利用規約
5 |
6 | );
7 | }
8 |
--------------------------------------------------------------------------------
/packages/training-web-1/src/styles/globals.css:
--------------------------------------------------------------------------------
1 | * {
2 | box-sizing: border-box;
3 | margin: 0;
4 | padding: 0;
5 | font-family: 'Hiragino Kaku Gothic ProN', 'Arial', 'Helvetica', sans-serif;
6 | word-break: break-all;
7 | }
8 |
9 | html,
10 | body {
11 | max-width: 100vw;
12 | }
13 |
14 | a {
15 | color: inherit;
16 | color: blue;
17 | }
--------------------------------------------------------------------------------
/packages/training-web-1/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "lib": [
5 | "dom",
6 | "dom.iterable",
7 | "esnext"
8 | ],
9 | "allowJs": true,
10 | "skipLibCheck": true,
11 | "strict": true,
12 | "forceConsistentCasingInFileNames": true,
13 | "noEmit": true,
14 | "esModuleInterop": true,
15 | "module": "esnext",
16 | "moduleResolution": "node",
17 | "resolveJsonModule": true,
18 | "isolatedModules": true,
19 | "jsx": "preserve",
20 | "incremental": true,
21 | "plugins": [
22 | {
23 | "name": "next"
24 | }
25 | ],
26 | "paths": {
27 | "@/*": [
28 | "./src/*"
29 | ]
30 | }
31 | },
32 | "include": [
33 | "next-env.d.ts",
34 | "**/*.ts",
35 | "**/*.tsx",
36 | ".next/types/**/*.ts"
37 | ],
38 | "exclude": [
39 | "node_modules"
40 | ]
41 | }
42 |
--------------------------------------------------------------------------------
/packages/training-web-2/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "parser": "@typescript-eslint/parser",
3 | "parserOptions": {
4 | "sourceType": "module",
5 | "project": "./tsconfig.json"
6 | },
7 | "plugins": [
8 | "@typescript-eslint",
9 | "unused-imports"
10 | ],
11 | "extends": [
12 | "next/core-web-vitals",
13 | "plugin:@typescript-eslint/recommended",
14 | "plugin:import/recommended",
15 | "prettier"
16 | ],
17 | "rules": {
18 | "import/order": [
19 | "error",
20 | {
21 | "groups": [
22 | "builtin",
23 | "external",
24 | "parent",
25 | "sibling",
26 | "index",
27 | "object",
28 | "type"
29 | ],
30 | "pathGroups": [
31 | {
32 | "pattern": "react",
33 | "group": "builtin",
34 | "position": "before"
35 | },
36 | {
37 | "pattern": "@/**",
38 | "group": "parent",
39 | "position": "before"
40 | }
41 | ],
42 | "pathGroupsExcludedImportTypes": [
43 | "builtin"
44 | ],
45 | "alphabetize": {
46 | "order": "asc"
47 | },
48 | "newlines-between": "never"
49 | }
50 | ],
51 | "@typescript-eslint/consistent-type-imports": "warn",
52 | "unused-imports/no-unused-imports": "error"
53 | }
54 | }
--------------------------------------------------------------------------------
/packages/training-web-2/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # next.js
12 | /.next/
13 | /out/
14 |
15 | # production
16 | /build
17 |
18 | # misc
19 | .DS_Store
20 | *.pem
21 |
22 | # debug
23 | npm-debug.log*
24 | yarn-debug.log*
25 | yarn-error.log*
26 |
27 | # local env files
28 | .env*.local
29 |
30 | # vercel
31 | .vercel
32 |
33 | # typescript
34 | *.tsbuildinfo
35 | next-env.d.ts
36 |
--------------------------------------------------------------------------------
/packages/training-web-2/README.md:
--------------------------------------------------------------------------------
1 | # training/training-web-2
2 |
3 | App Router 練習用プロジェクト
4 |
5 | - 1-4.ネスト可能なレイアウト
6 | - 2-1.Server Component と Client Component
7 |
--------------------------------------------------------------------------------
/packages/training-web-2/next.config.mjs:
--------------------------------------------------------------------------------
1 | /** @type {import('next').NextConfig} */
2 | const nextConfig = {
3 | }
4 |
5 | export default nextConfig;
6 |
--------------------------------------------------------------------------------
/packages/training-web-2/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "training-web-2",
3 | "version": "0.1.0",
4 | "private": true,
5 | "description": "App Router 練習アプリ(第1部4章〜5章)",
6 | "author": "takepepe",
7 | "license": "MIT",
8 | "scripts": {
9 | "dev": "next dev",
10 | "build": "next build",
11 | "start": "next start",
12 | "clean": "rimraf .next/cache/fetch-cache",
13 | "clean-dev": "run-s clean dev",
14 | "clean-build": "run-s clean build",
15 | "clean-start": "run-s clean build start",
16 | "lint": "next lint",
17 | "typecheck": "tsc --noEmit",
18 | "format": "run-s format:*",
19 | "format:elint": "eslint --fix './src/**/*.{js,ts,tsx}'",
20 | "format:prettier": "prettier --write './src/**/*.{js,ts,tsx}'"
21 | },
22 | "dependencies": {
23 | "next": "14.2.2",
24 | "react": "^18",
25 | "react-dom": "^18"
26 | },
27 | "devDependencies": {
28 | "@types/node": "^20",
29 | "@types/react": "^18",
30 | "@types/react-dom": "^18",
31 | "@typescript-eslint/eslint-plugin": "^6.9.0",
32 | "@typescript-eslint/parser": "^6.9.0",
33 | "eslint": "^8",
34 | "eslint-config-next": "14.2.2",
35 | "eslint-config-prettier": "^8.8.0",
36 | "eslint-plugin-unused-imports": "^3.0.0",
37 | "prettier": "^3.0.0",
38 | "ts-node": "^10.9.1",
39 | "typescript": "^5.2.2"
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/packages/training-web-2/src/app/_components/Footer/index.tsx:
--------------------------------------------------------------------------------
1 | import Link from "next/link";
2 | import styles from "./style.module.css";
3 |
4 | export function Footer() {
5 | return (
6 |
16 | );
17 | }
18 |
--------------------------------------------------------------------------------
/packages/training-web-2/src/app/_components/Footer/style.module.css:
--------------------------------------------------------------------------------
1 | .footer {
2 | padding: 24px;
3 | border-top: 1px solid #000;
4 | }
--------------------------------------------------------------------------------
/packages/training-web-2/src/app/_components/Header/index.tsx:
--------------------------------------------------------------------------------
1 | import Link from "next/link";
2 | import styles from "./style.module.css";
3 |
4 | export function Header() {
5 | return (
6 |
7 |
8 | Photo Share
9 |
10 |
11 | );
12 | }
13 |
--------------------------------------------------------------------------------
/packages/training-web-2/src/app/_components/Header/style.module.css:
--------------------------------------------------------------------------------
1 | .header {
2 | padding: 24px;
3 | border-bottom: 1px solid #000;
4 | }
--------------------------------------------------------------------------------
/packages/training-web-2/src/app/_components/Nav/IconButton.tsx:
--------------------------------------------------------------------------------
1 | "use client"; // ★: 不要な "use client"
2 |
3 | import styles from "./style.module.css";
4 |
5 | type Props = {
6 | onClick: () => void;
7 | children: React.ReactNode;
8 | };
9 | export function IconButton({ onClick, children }: Props) {
10 | return (
11 |
14 | );
15 | }
16 |
--------------------------------------------------------------------------------
/packages/training-web-2/src/app/_components/Nav/index.tsx:
--------------------------------------------------------------------------------
1 | "use client"; // ★: "use client" ディレクティブを追加する
2 |
3 | import Link from "next/link";
4 | import { usePathname } from "next/navigation";
5 | import styles from "./style.module.css";
6 |
7 | function getAriaCurrent(flag: boolean) {
8 | return flag ? { "aria-current": "page" as const } : undefined;
9 | }
10 |
11 | export function Nav() {
12 | // ★: usePathname Hook を使用して、現在のパスを取得したい
13 | const pathName = usePathname();
14 | return (
15 |
32 | );
33 | }
34 |
--------------------------------------------------------------------------------
/packages/training-web-2/src/app/_components/Nav/style.module.css:
--------------------------------------------------------------------------------
1 | .nav {
2 | min-width: 220px;
3 | padding: 24px;
4 | border-right: 1px solid #000;
5 | }
6 |
7 | .nav [aria-current="page"] {
8 | color: orangered;
9 | }
--------------------------------------------------------------------------------
/packages/training-web-2/src/app/categories/[categoryName]/page.module.css:
--------------------------------------------------------------------------------
1 | .pagination {
2 | border-top: 1px solid #000;
3 | }
--------------------------------------------------------------------------------
/packages/training-web-2/src/app/categories/[categoryName]/page.tsx:
--------------------------------------------------------------------------------
1 | import Link from "next/link";
2 | import { getPage } from "@/utils";
3 | import styles from "./page.module.css";
4 |
5 | type Props = {
6 | params: { categoryName: string };
7 | searchParams: { [key: string]: string | string[] | undefined };
8 | };
9 |
10 | export default async function Page({ params, searchParams }: Props) {
11 | const page = getPage(searchParams);
12 | return (
13 |
14 |
カテゴリー別一覧画面
15 |
16 | カテゴリー「{params.categoryName}」の「{page}」ページ目
17 |
18 |
19 | -
20 | 写真1
21 |
22 | -
23 | 写真2
24 |
25 | -
26 | 写真3
27 |
28 |
29 |
30 | {page !== 1 && (
31 | -
32 |
33 | 前へ
34 |
35 |
36 | )}
37 | -
38 |
39 | 次へ
40 |
41 |
42 |
43 |
44 | );
45 | }
46 |
--------------------------------------------------------------------------------
/packages/training-web-2/src/app/categories/layout.module.css:
--------------------------------------------------------------------------------
1 | .container {
2 | flex-grow: 1;
3 | padding: 24px;
4 | background-color: lightgray;
5 | }
--------------------------------------------------------------------------------
/packages/training-web-2/src/app/categories/layout.tsx:
--------------------------------------------------------------------------------
1 | import styles from "./layout.module.css";
2 |
3 | type Props = {
4 | children: React.ReactNode;
5 | };
6 |
7 | // ★:「/categories」配下で全適用されるレイアウト
8 | export default function Layout({ children }: Props) {
9 | return {children}
;
10 | }
11 |
--------------------------------------------------------------------------------
/packages/training-web-2/src/app/categories/page.tsx:
--------------------------------------------------------------------------------
1 | import Link from "next/link";
2 |
3 | export default async function Page() {
4 | return (
5 |
6 |
カテゴリー一覧画面
7 |
8 | -
9 | 花
10 |
11 | -
12 | 動物
13 |
14 | -
15 | 風景
16 |
17 |
18 |
19 | );
20 | }
21 |
--------------------------------------------------------------------------------
/packages/training-web-2/src/app/company-info/page.tsx:
--------------------------------------------------------------------------------
1 | export default function Page() {
2 | return (
3 |
4 |
企業概要
5 |
6 | );
7 | }
8 |
--------------------------------------------------------------------------------
/packages/training-web-2/src/app/layout.module.css:
--------------------------------------------------------------------------------
1 | .container {
2 | display: flex;
3 | flex-direction: column;
4 | min-height: 100vh;
5 | background-color: #d8dcea;
6 | }
7 |
8 | .content {
9 | flex-grow: 1;
10 | display: flex;
11 | }
12 |
13 | .main {
14 | flex-grow: 1;
15 | display: flex;
16 | }
--------------------------------------------------------------------------------
/packages/training-web-2/src/app/layout.tsx:
--------------------------------------------------------------------------------
1 | import "../styles/globals.css";
2 | import { Footer } from "./_components/Footer";
3 | import { Header } from "./_components/Header";
4 | import { Nav } from "./_components/Nav";
5 | import styles from "./layout.module.css";
6 |
7 | export default async function RootLayout({
8 | children,
9 | }: {
10 | children: React.ReactNode;
11 | }) {
12 | // ★:全ての画面に適用される共通レイアウト
13 | return (
14 |
15 |
16 |
17 |
18 |
19 |
20 | {children}
21 |
22 |
23 |
24 |
25 |
26 | );
27 | }
28 |
--------------------------------------------------------------------------------
/packages/training-web-2/src/app/page.module.css:
--------------------------------------------------------------------------------
1 | .container {
2 | flex-grow: 1;
3 | padding: 24px;
4 | background-color: #e0e3e9;
5 | }
--------------------------------------------------------------------------------
/packages/training-web-2/src/app/page.tsx:
--------------------------------------------------------------------------------
1 | import Link from "next/link";
2 | import styles from "./page.module.css";
3 |
4 | export default async function Page() {
5 | return (
6 |
7 |
トップ画面
8 |
9 | -
10 | 写真1
11 |
12 | -
13 | 写真2
14 |
15 | -
16 | 写真3
17 |
18 |
19 |
20 | );
21 | }
22 |
--------------------------------------------------------------------------------
/packages/training-web-2/src/app/photos/[photoId]/LikeButton.tsx:
--------------------------------------------------------------------------------
1 | "use client"; // ★: "use client" ディレクティブを追加する
2 |
3 | export function LikeButton({ photoId }: { photoId: string }) {
4 | // ★: onClick イベントハンドラーを追加したい
5 | return (
6 |
13 | );
14 | }
15 |
--------------------------------------------------------------------------------
/packages/training-web-2/src/app/photos/[photoId]/page.module.css:
--------------------------------------------------------------------------------
1 | .table {
2 | border-collapse: collapse;
3 | }
4 |
5 | .table th {
6 | min-width: 120px;
7 | }
8 |
9 | .table th,
10 | .table td {
11 | padding: 1em;
12 | border: 1px solid #000;
13 | }
--------------------------------------------------------------------------------
/packages/training-web-2/src/app/photos/[photoId]/page.tsx:
--------------------------------------------------------------------------------
1 | import Link from "next/link";
2 | import { LikeButton } from "./LikeButton";
3 | import styles from "./page.module.css";
4 |
5 | type Props = {
6 | params: { photoId: string };
7 | };
8 |
9 | export default async function Page({ params }: Props) {
10 | return (
11 |
12 |
写真ID「{params.photoId}」の詳細画面
13 |
14 |
15 |
16 | 概要 |
17 | 概要テキスト |
18 |
19 |
20 | カテゴリー |
21 |
22 | 花
23 | |
24 |
25 |
26 |
27 |
28 |
29 | );
30 | }
31 |
--------------------------------------------------------------------------------
/packages/training-web-2/src/app/photos/layout.module.css:
--------------------------------------------------------------------------------
1 | .container {
2 | flex-grow: 1;
3 | padding: 24px;
4 | background-color: lightblue;
5 | }
--------------------------------------------------------------------------------
/packages/training-web-2/src/app/photos/layout.tsx:
--------------------------------------------------------------------------------
1 | import styles from "./layout.module.css";
2 |
3 | type Props = {
4 | children: React.ReactNode;
5 | };
6 |
7 | // 「/photos」配下で全適用されるレイアウト
8 | export default function Layout({ children }: Props) {
9 | return {children}
;
10 | }
11 |
--------------------------------------------------------------------------------
/packages/training-web-2/src/app/privacy-policy/page.tsx:
--------------------------------------------------------------------------------
1 | export default function Page() {
2 | return (
3 |
4 |
利用規約
5 |
6 | );
7 | }
8 |
--------------------------------------------------------------------------------
/packages/training-web-2/src/constants.ts:
--------------------------------------------------------------------------------
1 | export const SITE_NAME = "Photo Share";
2 |
--------------------------------------------------------------------------------
/packages/training-web-2/src/styles/globals.css:
--------------------------------------------------------------------------------
1 | * {
2 | box-sizing: border-box;
3 | margin: 0;
4 | padding: 0;
5 | font-family: 'Hiragino Kaku Gothic ProN', 'Arial', 'Helvetica', sans-serif;
6 | word-break: break-all;
7 | }
8 |
9 | html,
10 | body {
11 | max-width: 100vw;
12 | }
13 |
14 | a {
15 | color: inherit;
16 | color: blue;
17 | }
--------------------------------------------------------------------------------
/packages/training-web-2/src/utils/index.ts:
--------------------------------------------------------------------------------
1 | export function getPage(searchParams: {
2 | [key: string]: string | string[] | undefined;
3 | }) {
4 | if (typeof searchParams.page !== "string") return 1;
5 | const page = parseInt(searchParams.page);
6 | if (isNaN(page)) return 1;
7 | if (page < 1) return 1;
8 | return page;
9 | }
10 |
--------------------------------------------------------------------------------
/packages/training-web-2/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "lib": [
5 | "dom",
6 | "dom.iterable",
7 | "esnext"
8 | ],
9 | "allowJs": true,
10 | "skipLibCheck": true,
11 | "strict": true,
12 | "forceConsistentCasingInFileNames": true,
13 | "noEmit": true,
14 | "esModuleInterop": true,
15 | "module": "esnext",
16 | "moduleResolution": "node",
17 | "resolveJsonModule": true,
18 | "isolatedModules": true,
19 | "jsx": "preserve",
20 | "incremental": true,
21 | "plugins": [
22 | {
23 | "name": "next"
24 | }
25 | ],
26 | "paths": {
27 | "@/*": [
28 | "./src/*"
29 | ]
30 | }
31 | },
32 | "include": [
33 | "next-env.d.ts",
34 | "**/*.ts",
35 | "**/*.tsx",
36 | ".next/types/**/*.ts"
37 | ],
38 | "exclude": [
39 | "node_modules"
40 | ]
41 | }
42 |
--------------------------------------------------------------------------------
/packages/training-web-3/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "parser": "@typescript-eslint/parser",
3 | "parserOptions": {
4 | "sourceType": "module",
5 | "project": "./tsconfig.json"
6 | },
7 | "plugins": [
8 | "@typescript-eslint",
9 | "unused-imports"
10 | ],
11 | "extends": [
12 | "next/core-web-vitals",
13 | "plugin:@typescript-eslint/recommended",
14 | "plugin:import/recommended",
15 | "prettier"
16 | ],
17 | "rules": {
18 | "import/order": [
19 | "error",
20 | {
21 | "groups": [
22 | "builtin",
23 | "external",
24 | "parent",
25 | "sibling",
26 | "index",
27 | "object",
28 | "type"
29 | ],
30 | "pathGroups": [
31 | {
32 | "pattern": "react",
33 | "group": "builtin",
34 | "position": "before"
35 | },
36 | {
37 | "pattern": "@/**",
38 | "group": "parent",
39 | "position": "before"
40 | }
41 | ],
42 | "pathGroupsExcludedImportTypes": [
43 | "builtin"
44 | ],
45 | "alphabetize": {
46 | "order": "asc"
47 | },
48 | "newlines-between": "never"
49 | }
50 | ],
51 | "@typescript-eslint/consistent-type-imports": "warn",
52 | "unused-imports/no-unused-imports": "error"
53 | }
54 | }
--------------------------------------------------------------------------------
/packages/training-web-3/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # next.js
12 | /.next/
13 | /out/
14 |
15 | # production
16 | /build
17 |
18 | # misc
19 | .DS_Store
20 | *.pem
21 |
22 | # debug
23 | npm-debug.log*
24 | yarn-debug.log*
25 | yarn-error.log*
26 |
27 | # local env files
28 | .env*.local
29 |
30 | # vercel
31 | .vercel
32 |
33 | # typescript
34 | *.tsbuildinfo
35 | next-env.d.ts
36 |
--------------------------------------------------------------------------------
/packages/training-web-3/README.md:
--------------------------------------------------------------------------------
1 | # training/training-web-3
2 |
3 | App Router 練習用プロジェクト
4 |
5 | - 2-2.Server Component のデータ取得
6 | - 2-3.動的データ取得・静的データ取得
7 | - 2-4.Route のレンダリング
8 |
--------------------------------------------------------------------------------
/packages/training-web-3/next.config.mjs:
--------------------------------------------------------------------------------
1 | /** @type {import('next').NextConfig} */
2 | const nextConfig = {
3 | }
4 |
5 | export default nextConfig;
6 |
--------------------------------------------------------------------------------
/packages/training-web-3/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "training-web-3",
3 | "version": "0.1.0",
4 | "private": true,
5 | "description": "App Router 練習アプリ(第1部6章〜7章)",
6 | "author": "takepepe",
7 | "license": "MIT",
8 | "scripts": {
9 | "dev": "next dev",
10 | "build": "next build",
11 | "start": "next start",
12 | "clean": "rimraf .next/cache/fetch-cache",
13 | "clean-dev": "run-s clean dev",
14 | "clean-build": "run-s clean build",
15 | "clean-start": "run-s clean build start",
16 | "lint": "next lint",
17 | "typecheck": "tsc --noEmit",
18 | "format": "run-s format:*",
19 | "format:elint": "eslint --fix './src/**/*.{js,ts,tsx}'",
20 | "format:prettier": "prettier --write './src/**/*.{js,ts,tsx}'"
21 | },
22 | "dependencies": {
23 | "next": "14.2.2",
24 | "react": "^18",
25 | "react-dom": "^18"
26 | },
27 | "devDependencies": {
28 | "@types/node": "^20",
29 | "@types/react": "^18",
30 | "@types/react-dom": "^18",
31 | "@typescript-eslint/eslint-plugin": "^6.9.0",
32 | "@typescript-eslint/parser": "^6.9.0",
33 | "eslint": "^8",
34 | "eslint-config-next": "14.2.2",
35 | "eslint-config-prettier": "^8.8.0",
36 | "eslint-plugin-unused-imports": "^3.0.0",
37 | "prettier": "^3.0.0",
38 | "ts-node": "^10.9.1",
39 | "typescript": "^5.2.2"
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/packages/training-web-3/src/app/_components/Footer/index.tsx:
--------------------------------------------------------------------------------
1 | import Link from "next/link";
2 | import styles from "./style.module.css";
3 |
4 | export function Footer() {
5 | return (
6 |
16 | );
17 | }
18 |
--------------------------------------------------------------------------------
/packages/training-web-3/src/app/_components/Footer/style.module.css:
--------------------------------------------------------------------------------
1 | .footer {
2 | padding: 24px;
3 | border-top: 1px solid #000;
4 | }
--------------------------------------------------------------------------------
/packages/training-web-3/src/app/_components/Header/index.tsx:
--------------------------------------------------------------------------------
1 | import Link from "next/link";
2 | import styles from "./style.module.css";
3 |
4 | export function Header() {
5 | return (
6 |
7 |
8 | Photo Share
9 |
10 |
11 | );
12 | }
13 |
--------------------------------------------------------------------------------
/packages/training-web-3/src/app/_components/Header/style.module.css:
--------------------------------------------------------------------------------
1 | .header {
2 | padding: 24px;
3 | border-bottom: 1px solid #000;
4 | }
--------------------------------------------------------------------------------
/packages/training-web-3/src/app/_components/Nav/index.tsx:
--------------------------------------------------------------------------------
1 | "use client"; // ★: "use client" ディレクティブを追加する
2 |
3 | import Link from "next/link";
4 | import { usePathname } from "next/navigation";
5 | import styles from "./style.module.css";
6 |
7 | function getAriaCurrent(flag: boolean) {
8 | return flag ? { "aria-current": "page" as const } : undefined;
9 | }
10 |
11 | export function Nav() {
12 | // ★: usePathname Hook を使用して、現在のパスを取得したい
13 | const pathName = usePathname();
14 | return (
15 |
32 | );
33 | }
34 |
--------------------------------------------------------------------------------
/packages/training-web-3/src/app/_components/Nav/style.module.css:
--------------------------------------------------------------------------------
1 | .nav {
2 | min-width: 220px;
3 | padding: 24px;
4 | border-right: 1px solid #000;
5 | }
6 |
7 | .nav [aria-current="page"] {
8 | color: orangered;
9 | }
--------------------------------------------------------------------------------
/packages/training-web-3/src/app/categories/[categoryName]/page.module.css:
--------------------------------------------------------------------------------
1 | .pagination {
2 | border-top: 1px solid #000;
3 | }
--------------------------------------------------------------------------------
/packages/training-web-3/src/app/categories/[categoryName]/page.tsx:
--------------------------------------------------------------------------------
1 | import Link from "next/link";
2 | import type { Category, Photo } from "@/type";
3 | import { getPage } from "@/utils";
4 | import styles from "./page.module.css";
5 |
6 | async function getCategory(categoryName: string) {
7 | const data: { category: Category } = await fetch(
8 | `http://localhost:8080/api/categories/${categoryName}`
9 | ).then((res) => res.json());
10 | return data.category;
11 | }
12 |
13 | async function getPhotos() {
14 | const data: { photos: Photo[] } = await fetch(
15 | "http://localhost:8080/api/photos"
16 | ).then((res) => res.json());
17 | return data.photos;
18 | }
19 |
20 | type Props = {
21 | params: { categoryName: string };
22 | searchParams: { [key: string]: string | string[] | undefined };
23 | };
24 |
25 | export default async function Page({ params, searchParams }: Props) {
26 | // ★: Promise.all を使用した並列データ取得
27 | const [category, photos] = await Promise.all([
28 | getCategory(params.categoryName),
29 | getPhotos(),
30 | ]);
31 | // 🚧: 本来であれば、カテゴリーに紐づく写真のみを取得しページネーションを施す
32 | const page = getPage(searchParams);
33 | return (
34 |
35 |
36 | カテゴリー「{category.label}」の「{page}」ページ目
37 |
38 |
39 | {photos
40 | .filter((photo) => photo.categoryId === category.id)
41 | .map((photo) => (
42 | -
43 | {photo.title}
44 |
45 | ))}
46 |
47 |
48 | {page !== 1 && (
49 | -
50 |
51 | 前へ
52 |
53 |
54 | )}
55 | -
56 |
57 | 次へ
58 |
59 |
60 |
61 |
62 | );
63 | }
64 |
--------------------------------------------------------------------------------
/packages/training-web-3/src/app/categories/layout.module.css:
--------------------------------------------------------------------------------
1 | .container {
2 | flex-grow: 1;
3 | padding: 24px;
4 | background-color: lightgray;
5 | }
--------------------------------------------------------------------------------
/packages/training-web-3/src/app/categories/layout.tsx:
--------------------------------------------------------------------------------
1 | import styles from "./layout.module.css";
2 |
3 | type Props = {
4 | children: React.ReactNode;
5 | };
6 |
7 | // ★:「/categories」配下で全適用されるレイアウト
8 | export default function Layout({ children }: Props) {
9 | return {children}
;
10 | }
11 |
--------------------------------------------------------------------------------
/packages/training-web-3/src/app/categories/page.tsx:
--------------------------------------------------------------------------------
1 | import Link from "next/link";
2 | import type { Category } from "@/type";
3 |
4 | async function getCategories() {
5 | const data: { categories: Category[] } = await fetch(
6 | `http://localhost:8080/api/categories`,
7 | ).then((res) => res.json());
8 | return data.categories;
9 | }
10 |
11 | export default async function Page() {
12 | const categories = await getCategories();
13 | return (
14 |
15 |
カテゴリー一覧画面
16 |
17 | {categories.map(({ id, label, name }) => (
18 | -
19 | {label}
20 |
21 | ))}
22 |
23 |
24 | );
25 | }
26 |
--------------------------------------------------------------------------------
/packages/training-web-3/src/app/company-info/page.tsx:
--------------------------------------------------------------------------------
1 | export default function Page() {
2 | return (
3 |
4 |
企業概要
5 |
6 | );
7 | }
8 |
--------------------------------------------------------------------------------
/packages/training-web-3/src/app/layout.module.css:
--------------------------------------------------------------------------------
1 | .container {
2 | display: flex;
3 | flex-direction: column;
4 | min-height: 100vh;
5 | background-color: #d8dcea;
6 | }
7 |
8 | .content {
9 | flex-grow: 1;
10 | display: flex;
11 | }
12 |
13 | .main {
14 | flex-grow: 1;
15 | display: flex;
16 | }
--------------------------------------------------------------------------------
/packages/training-web-3/src/app/layout.tsx:
--------------------------------------------------------------------------------
1 | import "../styles/globals.css";
2 | import { Footer } from "./_components/Footer";
3 | import { Header } from "./_components/Header";
4 | import { Nav } from "./_components/Nav";
5 | import styles from "./layout.module.css";
6 |
7 | export default async function RootLayout({
8 | children,
9 | }: {
10 | children: React.ReactNode;
11 | }) {
12 | // ★:全ての画面に適用される共通レイアウト
13 | return (
14 |
15 |
16 |
17 |
18 |
19 |
20 | {children}
21 |
22 |
23 |
24 |
25 |
26 | );
27 | }
28 |
--------------------------------------------------------------------------------
/packages/training-web-3/src/app/page.module.css:
--------------------------------------------------------------------------------
1 | .container {
2 | flex-grow: 1;
3 | padding: 24px;
4 | background-color: #e0e3e9;
5 | }
--------------------------------------------------------------------------------
/packages/training-web-3/src/app/page.tsx:
--------------------------------------------------------------------------------
1 | import Link from "next/link";
2 | import type { Photo } from "@/type";
3 | import styles from "./page.module.css";
4 |
5 | async function getPhotos() {
6 | const data: { photos: Photo[] } = await fetch(
7 | "http://localhost:8080/api/photos"
8 | ).then((res) => res.json());
9 | return data.photos.map(({ id, title }) => ({ id, title }));
10 | }
11 |
12 | export default async function Page() {
13 | const photos = await getPhotos(); // <- データを取得
14 | return (
15 |
16 |
トップ画面
17 |
18 | {photos.map(({ id, title }) => (
19 | -
20 | {title}
21 |
22 | ))}
23 |
24 |
25 | );
26 | }
27 |
--------------------------------------------------------------------------------
/packages/training-web-3/src/app/photos/[photoId]/LikeButton.tsx:
--------------------------------------------------------------------------------
1 | "use client"; // ★: "use client" ディレクティブを追加する
2 |
3 | export function LikeButton({ photoId }: { photoId: string }) {
4 | // ★: onClick イベントハンドラーを追加したい
5 | return (
6 |
13 | );
14 | }
15 |
--------------------------------------------------------------------------------
/packages/training-web-3/src/app/photos/[photoId]/page.module.css:
--------------------------------------------------------------------------------
1 | .table {
2 | border-collapse: collapse;
3 | }
4 |
5 | .table th {
6 | min-width: 120px;
7 | }
8 |
9 | .table th,
10 | .table td {
11 | padding: 1em;
12 | border: 1px solid #000;
13 | }
--------------------------------------------------------------------------------
/packages/training-web-3/src/app/photos/[photoId]/page.tsx:
--------------------------------------------------------------------------------
1 | import Link from "next/link";
2 | import type { Category, Photo } from "@/type";
3 | import { LikeButton } from "./LikeButton";
4 | import styles from "./page.module.css";
5 |
6 | async function getPhoto(photoId: string) {
7 | const data: { photo: Photo } = await fetch(
8 | `http://localhost:8080/api/photos/${photoId}`,
9 | ).then((res) => res.json());
10 | return data.photo;
11 | }
12 |
13 | async function getCategory(categoryId: string) {
14 | const data: { category: Category } = await fetch(
15 | `http://localhost:8080/api/categories/id/${categoryId}`,
16 | ).then((res) => res.json());
17 | return data.category;
18 | }
19 |
20 | type Props = {
21 | params: { photoId: string };
22 | };
23 |
24 | export default async function Page({ params }: Props) {
25 | const photo = await getPhoto(params.photoId);
26 | const category = await getCategory(photo.categoryId);
27 | return (
28 |
29 |
写真ID「{photo.title}」の詳細画面
30 |
31 |
32 |
33 | 概要 |
34 | {photo.description} |
35 |
36 |
37 | カテゴリー |
38 |
39 |
40 | {category.label}
41 |
42 | |
43 |
44 |
45 |
46 |
47 |
48 | );
49 | }
50 |
--------------------------------------------------------------------------------
/packages/training-web-3/src/app/photos/layout.module.css:
--------------------------------------------------------------------------------
1 | .container {
2 | flex-grow: 1;
3 | padding: 24px;
4 | background-color: lightblue;
5 | }
--------------------------------------------------------------------------------
/packages/training-web-3/src/app/photos/layout.tsx:
--------------------------------------------------------------------------------
1 | import styles from "./layout.module.css";
2 |
3 | type Props = {
4 | children: React.ReactNode;
5 | };
6 |
7 | // 「/photos」配下で全適用されるレイアウト
8 | export default function Layout({ children }: Props) {
9 | return {children}
;
10 | }
11 |
--------------------------------------------------------------------------------
/packages/training-web-3/src/app/privacy-policy/page.tsx:
--------------------------------------------------------------------------------
1 | export default function Page() {
2 | return (
3 |
4 |
利用規約
5 |
6 | );
7 | }
8 |
--------------------------------------------------------------------------------
/packages/training-web-3/src/constants.ts:
--------------------------------------------------------------------------------
1 | export const SITE_NAME = "Photo Share";
2 |
--------------------------------------------------------------------------------
/packages/training-web-3/src/styles/globals.css:
--------------------------------------------------------------------------------
1 | * {
2 | box-sizing: border-box;
3 | margin: 0;
4 | padding: 0;
5 | font-family: 'Hiragino Kaku Gothic ProN', 'Arial', 'Helvetica', sans-serif;
6 | word-break: break-all;
7 | }
8 |
9 | html,
10 | body {
11 | max-width: 100vw;
12 | }
13 |
14 | a {
15 | color: inherit;
16 | color: blue;
17 | }
--------------------------------------------------------------------------------
/packages/training-web-3/src/type.ts:
--------------------------------------------------------------------------------
1 | export type Photo = {
2 | id: string;
3 | title: string;
4 | description: string;
5 | imageUrl: string;
6 | authorId: string;
7 | categoryId: string;
8 | };
9 |
10 | export type Category = {
11 | id: string;
12 | name: string;
13 | label: string;
14 | description: string;
15 | imageUrl: string;
16 | };
17 |
--------------------------------------------------------------------------------
/packages/training-web-3/src/utils/index.ts:
--------------------------------------------------------------------------------
1 | export function getPage(searchParams: {
2 | [key: string]: string | string[] | undefined;
3 | }) {
4 | if (typeof searchParams.page !== "string") return 1;
5 | const page = parseInt(searchParams.page);
6 | if (isNaN(page)) return 1;
7 | if (page < 1) return 1;
8 | return page;
9 | }
10 |
--------------------------------------------------------------------------------
/packages/training-web-3/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "lib": [
5 | "dom",
6 | "dom.iterable",
7 | "esnext"
8 | ],
9 | "allowJs": true,
10 | "skipLibCheck": true,
11 | "strict": true,
12 | "forceConsistentCasingInFileNames": true,
13 | "noEmit": true,
14 | "esModuleInterop": true,
15 | "module": "esnext",
16 | "moduleResolution": "node",
17 | "resolveJsonModule": true,
18 | "isolatedModules": true,
19 | "jsx": "preserve",
20 | "incremental": true,
21 | "plugins": [
22 | {
23 | "name": "next"
24 | }
25 | ],
26 | "paths": {
27 | "@/*": [
28 | "./src/*"
29 | ]
30 | }
31 | },
32 | "include": [
33 | "next-env.d.ts",
34 | "**/*.ts",
35 | "**/*.tsx",
36 | ".next/types/**/*.ts"
37 | ],
38 | "exclude": [
39 | "node_modules"
40 | ]
41 | }
42 |
--------------------------------------------------------------------------------
/packages/training-web-4/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "parser": "@typescript-eslint/parser",
3 | "parserOptions": {
4 | "sourceType": "module",
5 | "project": "./tsconfig.json"
6 | },
7 | "plugins": [
8 | "@typescript-eslint",
9 | "unused-imports"
10 | ],
11 | "extends": [
12 | "next/core-web-vitals",
13 | "plugin:@typescript-eslint/recommended",
14 | "plugin:import/recommended",
15 | "prettier"
16 | ],
17 | "rules": {
18 | "import/order": [
19 | "error",
20 | {
21 | "groups": [
22 | "builtin",
23 | "external",
24 | "parent",
25 | "sibling",
26 | "index",
27 | "object",
28 | "type"
29 | ],
30 | "pathGroups": [
31 | {
32 | "pattern": "react",
33 | "group": "builtin",
34 | "position": "before"
35 | },
36 | {
37 | "pattern": "@/**",
38 | "group": "parent",
39 | "position": "before"
40 | }
41 | ],
42 | "pathGroupsExcludedImportTypes": [
43 | "builtin"
44 | ],
45 | "alphabetize": {
46 | "order": "asc"
47 | },
48 | "newlines-between": "never"
49 | }
50 | ],
51 | "@typescript-eslint/consistent-type-imports": "warn",
52 | "unused-imports/no-unused-imports": "error"
53 | }
54 | }
--------------------------------------------------------------------------------
/packages/training-web-4/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # next.js
12 | /.next/
13 | /out/
14 |
15 | # production
16 | /build
17 |
18 | # misc
19 | .DS_Store
20 | *.pem
21 |
22 | # debug
23 | npm-debug.log*
24 | yarn-debug.log*
25 | yarn-error.log*
26 |
27 | # local env files
28 | .env*.local
29 |
30 | # vercel
31 | .vercel
32 |
33 | # typescript
34 | *.tsbuildinfo
35 | next-env.d.ts
36 |
--------------------------------------------------------------------------------
/packages/training-web-4/README.md:
--------------------------------------------------------------------------------
1 | # training/training-web-4
2 |
3 | App Router 練習用プロジェクト
4 |
5 | - 3-1.Segment 構成ファイル
6 | - 3-2.Segment 構成フォルダ
7 | - 3-4.Route のメタデータ
8 | - 4.3:Route Handlerの使用例
9 |
--------------------------------------------------------------------------------
/packages/training-web-4/next.config.mjs:
--------------------------------------------------------------------------------
1 | /** @type {import('next').NextConfig} */
2 | const nextConfig = {
3 | }
4 |
5 | export default nextConfig;
6 |
--------------------------------------------------------------------------------
/packages/training-web-4/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "training-web-4",
3 | "version": "0.1.0",
4 | "private": true,
5 | "description": "App Router 練習アプリ(第1部9章〜11章)",
6 | "author": "takepepe",
7 | "license": "MIT",
8 | "scripts": {
9 | "dev": "next dev",
10 | "build": "next build",
11 | "start": "next start",
12 | "clean": "rimraf .next/cache/fetch-cache",
13 | "clean-dev": "run-s clean dev",
14 | "clean-build": "run-s clean build",
15 | "clean-start": "run-s clean build start",
16 | "lint": "next lint",
17 | "typecheck": "tsc --noEmit",
18 | "format": "run-s format:*",
19 | "format:elint": "eslint --fix './src/**/*.{js,ts,tsx}'",
20 | "format:prettier": "prettier --write './src/**/*.{js,ts,tsx}'"
21 | },
22 | "dependencies": {
23 | "next": "14.2.2",
24 | "react": "^18",
25 | "react-dom": "^18"
26 | },
27 | "devDependencies": {
28 | "@types/node": "^20",
29 | "@types/react": "^18",
30 | "@types/react-dom": "^18",
31 | "@typescript-eslint/eslint-plugin": "^6.9.0",
32 | "@typescript-eslint/parser": "^6.9.0",
33 | "eslint": "^8",
34 | "eslint-config-next": "14.2.2",
35 | "eslint-config-prettier": "^8.8.0",
36 | "eslint-plugin-unused-imports": "^3.0.0",
37 | "prettier": "^3.0.0",
38 | "ts-node": "^10.9.1",
39 | "typescript": "^5.2.2"
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/packages/training-web-4/src/app/(site)/_components/Nav/index.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import Link from "next/link";
4 | import { usePathname } from "next/navigation";
5 | import styles from "./style.module.css";
6 |
7 | function getAriaCurrent(flag: boolean) {
8 | return flag ? { "aria-current": "page" as const } : undefined;
9 | }
10 |
11 | export function Nav() {
12 | const pathName = usePathname();
13 | return (
14 |
31 | );
32 | }
33 |
--------------------------------------------------------------------------------
/packages/training-web-4/src/app/(site)/_components/Nav/style.module.css:
--------------------------------------------------------------------------------
1 | .nav {
2 | min-width: 220px;
3 | padding: 24px;
4 | border-right: 1px solid #000;
5 | }
6 |
7 | .nav [aria-current="page"] {
8 | color: orangered;
9 | }
--------------------------------------------------------------------------------
/packages/training-web-4/src/app/(site)/categories/[categoryName]/page.module.css:
--------------------------------------------------------------------------------
1 | .pagination {
2 | border-top: 1px solid #000;
3 | }
--------------------------------------------------------------------------------
/packages/training-web-4/src/app/(site)/categories/[categoryName]/page.tsx:
--------------------------------------------------------------------------------
1 | import Link from "next/link";
2 | import { notFound } from "next/navigation";
3 | import type { Category, Photo } from "@/type";
4 | import { getPage } from "@/utils";
5 | import styles from "./page.module.css";
6 | import type { Metadata } from "next";
7 |
8 | async function getCategory(categoryName: string) {
9 | const data: { category: Category } = await fetch(
10 | `http://localhost:8080/api/categories/${categoryName}`
11 | ).then((res) => {
12 | if (!res.ok) {
13 | notFound();
14 | }
15 | return res.json();
16 | });
17 | return data.category;
18 | }
19 |
20 | async function getPhotos() {
21 | const data: { photos: Photo[] } = await fetch(
22 | "http://localhost:8080/api/photos"
23 | ).then((res) => {
24 | if (!res.ok) {
25 | notFound();
26 | }
27 | return res.json();
28 | });
29 | return data.photos;
30 | }
31 |
32 | type Props = {
33 | params: { categoryName: string };
34 | searchParams: { [key: string]: string | string[] | undefined };
35 | };
36 |
37 | export async function generateMetadata({ params }: Props): Promise {
38 | const category = await getCategory(params.categoryName);
39 | return {
40 | title: `カテゴリー「${category.label}」の写真一覧`,
41 | };
42 | }
43 | export default async function Page({ params, searchParams }: Props) {
44 | // ★: Promise.all を使用した並列データ取得
45 | const [category, photos] = await Promise.all([
46 | getCategory(params.categoryName),
47 | getPhotos(),
48 | ]);
49 | // 🚧: 本来であれば、カテゴリーに紐づく写真のみを取得しページネーションを施す
50 | const page = getPage(searchParams);
51 | if (page > 10) {
52 | // 11ページ以降は404扱いにする
53 | notFound();
54 | }
55 | return (
56 |
57 |
58 | カテゴリー「{category.label}」の「{page}」ページ目
59 |
60 |
61 | {photos
62 | .filter((photo) => photo.categoryId === category.id)
63 | .map((photo) => (
64 | -
65 | {photo.title}
66 |
67 | ))}
68 |
69 |
70 | {page !== 1 && (
71 | -
72 |
73 | 前へ
74 |
75 |
76 | )}
77 | -
78 |
79 | 次へ
80 |
81 |
82 |
83 |
84 | );
85 | }
86 |
--------------------------------------------------------------------------------
/packages/training-web-4/src/app/(site)/categories/layout.module.css:
--------------------------------------------------------------------------------
1 | .container {
2 | flex-grow: 1;
3 | padding: 24px;
4 | background-color: lightgray;
5 | }
--------------------------------------------------------------------------------
/packages/training-web-4/src/app/(site)/categories/layout.tsx:
--------------------------------------------------------------------------------
1 | import styles from "./layout.module.css";
2 |
3 | type Props = {
4 | children: React.ReactNode;
5 | };
6 |
7 | // 「/categories」配下で全適用されるレイアウト
8 | export default function Layout({ children }: Props) {
9 | return {children}
;
10 | }
11 |
--------------------------------------------------------------------------------
/packages/training-web-4/src/app/(site)/categories/loading.tsx:
--------------------------------------------------------------------------------
1 | export default function Loading() {
2 | return <>...loading>;
3 | }
4 |
--------------------------------------------------------------------------------
/packages/training-web-4/src/app/(site)/categories/not-found.tsx:
--------------------------------------------------------------------------------
1 | export default async function NotFound() {
2 | return Not Found
;
3 | }
4 |
--------------------------------------------------------------------------------
/packages/training-web-4/src/app/(site)/categories/page.tsx:
--------------------------------------------------------------------------------
1 | import Link from "next/link";
2 | import type { Category } from "@/type";
3 |
4 | async function getCategories() {
5 | const data: { categories: Category[] } = await fetch(
6 | `http://localhost:8080/api/categories`,
7 | ).then((res) => res.json());
8 | return data.categories;
9 | }
10 |
11 | export default async function Page() {
12 | const categories = await getCategories();
13 | return (
14 |
15 |
カテゴリー一覧画面
16 |
17 | {categories.map(({ id, label, name }) => (
18 | -
19 | {label}
20 |
21 | ))}
22 |
23 |
24 | );
25 | }
26 |
--------------------------------------------------------------------------------
/packages/training-web-4/src/app/(site)/layout.module.css:
--------------------------------------------------------------------------------
1 | .container {
2 | display: flex;
3 | flex-direction: column;
4 | min-height: 100vh;
5 | background-color: #d8dcea;
6 | }
7 |
8 | .content {
9 | flex-grow: 1;
10 | display: flex;
11 | }
12 |
13 | .main {
14 | flex-grow: 1;
15 | display: flex;
16 | }
--------------------------------------------------------------------------------
/packages/training-web-4/src/app/(site)/layout.tsx:
--------------------------------------------------------------------------------
1 | // 「3-2-2-2.Route Groups 専用のレイアウトをネストする」で解説されている「Layout A」ファイル
2 |
3 | import { Footer } from "../_components/Footer";
4 | import { Header } from "../_components/Header";
5 | import { Nav } from "./_components/Nav";
6 | import styles from "./layout.module.css";
7 |
8 | type Props = {
9 | children: React.ReactNode;
10 | };
11 |
12 | // 動的要素が入り混り構成される(site)配下で全適用されるレイアウト
13 | export default function Layout({ children }: Props) {
14 | return (
15 |
16 |
17 |
18 |
19 | {children}
20 |
21 |
22 |
23 | );
24 | }
25 |
--------------------------------------------------------------------------------
/packages/training-web-4/src/app/(site)/page.module.css:
--------------------------------------------------------------------------------
1 | .container {
2 | flex-grow: 1;
3 | padding: 24px;
4 | background-color: #e0e3e9;
5 | }
--------------------------------------------------------------------------------
/packages/training-web-4/src/app/(site)/page.tsx:
--------------------------------------------------------------------------------
1 | import Link from "next/link";
2 | import type { Photo } from "@/type";
3 | import styles from "./page.module.css";
4 |
5 | async function getPhotos() {
6 | const data: { photos: Photo[] } = await fetch(
7 | "http://localhost:8080/api/photos"
8 | ).then((res) => res.json());
9 | return data.photos.map(({ id, title }) => ({ id, title }));
10 | }
11 |
12 | export default async function Page() {
13 | const photos = await getPhotos();
14 | return (
15 |
16 |
トップ画面
17 |
18 | {photos.map(({ id, title }) => (
19 | -
20 | {title}
21 |
22 | ))}
23 |
24 |
25 | );
26 | }
27 |
--------------------------------------------------------------------------------
/packages/training-web-4/src/app/(site)/photos/[photoId]/LikeButton.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | export function LikeButton({ photoId }: { photoId: string }) {
4 | return (
5 |
15 | );
16 | }
17 |
--------------------------------------------------------------------------------
/packages/training-web-4/src/app/(site)/photos/[photoId]/page.module.css:
--------------------------------------------------------------------------------
1 | .table {
2 | border-collapse: collapse;
3 | }
4 |
5 | .table th {
6 | min-width: 120px;
7 | }
8 |
9 | .table th,
10 | .table td {
11 | padding: 1em;
12 | border: 1px solid #000;
13 | }
--------------------------------------------------------------------------------
/packages/training-web-4/src/app/(site)/photos/[photoId]/page.tsx:
--------------------------------------------------------------------------------
1 | import Link from "next/link";
2 | import type { Category, Photo } from "@/type";
3 | import { LikeButton } from "./LikeButton";
4 | import styles from "./page.module.css";
5 | import type { Metadata } from "next";
6 |
7 | async function getPhoto(photoId: string) {
8 | const data: { photo: Photo } = await fetch(
9 | `http://localhost:8080/api/photos/${photoId}`,
10 | ).then((res) => res.json());
11 | return data.photo;
12 | }
13 |
14 | async function getCategory(categoryId: string) {
15 | const data: { category: Category } = await fetch(
16 | `http://localhost:8080/api/categories/id/${categoryId}`,
17 | ).then((res) => res.json());
18 | return data.category;
19 | }
20 |
21 | type Props = {
22 | params: { photoId: string };
23 | };
24 |
25 | export async function generateMetadata({ params }: Props): Promise {
26 | const photo = await getPhoto(params.photoId);
27 | return {
28 | title: photo.title,
29 | description: photo.description,
30 | };
31 | }
32 |
33 | export default async function Page({ params }: Props) {
34 | const photo = await getPhoto(params.photoId);
35 | const category = await getCategory(photo.categoryId);
36 | return (
37 |
38 |
写真ID「{photo.title}」の詳細画面
39 |
40 |
41 |
42 | 概要 |
43 | {photo.description} |
44 |
45 |
46 | カテゴリー |
47 |
48 |
49 | {category.label}
50 |
51 | |
52 |
53 |
54 |
55 |
56 |
57 | );
58 | }
59 |
--------------------------------------------------------------------------------
/packages/training-web-4/src/app/(site)/photos/layout.module.css:
--------------------------------------------------------------------------------
1 | .container {
2 | flex-grow: 1;
3 | padding: 24px;
4 | background-color: lightblue;
5 | }
--------------------------------------------------------------------------------
/packages/training-web-4/src/app/(site)/photos/layout.tsx:
--------------------------------------------------------------------------------
1 | import styles from "./layout.module.css";
2 |
3 | type Props = {
4 | children: React.ReactNode;
5 | };
6 |
7 | // 「/photos」配下で全適用されるレイアウト
8 | export default function Layout({ children }: Props) {
9 | return {children}
;
10 | }
11 |
--------------------------------------------------------------------------------
/packages/training-web-4/src/app/(site)/photos/loading.tsx:
--------------------------------------------------------------------------------
1 | export default function Loading() {
2 | return <>...loading>;
3 | }
4 |
--------------------------------------------------------------------------------
/packages/training-web-4/src/app/(site)/photos/not-found.tsx:
--------------------------------------------------------------------------------
1 | export default async function NotFound() {
2 | return Not Found
;
3 | }
4 |
--------------------------------------------------------------------------------
/packages/training-web-4/src/app/(static)/company-info/page.tsx:
--------------------------------------------------------------------------------
1 | import { SITE_NAME } from "@/constants";
2 | import type { Metadata } from "next";
3 |
4 | export const metadata: Metadata = {
5 | title: `運営企業 | ${SITE_NAME}`,
6 | description: "運営企業「テックピクチャーズ株式会社」の会社概要",
7 | };
8 |
9 | export default function Page() {
10 | return (
11 |
12 |
企業概要
13 |
14 | );
15 | }
16 |
--------------------------------------------------------------------------------
/packages/training-web-4/src/app/(static)/layout.module.css:
--------------------------------------------------------------------------------
1 | .container {
2 | display: flex;
3 | flex-direction: column;
4 | min-height: 100vh;
5 | background-color: lightyellow;
6 | }
7 |
8 | .content {
9 | flex-grow: 1;
10 | display: flex;
11 | }
12 |
13 | .main {
14 | flex-grow: 1;
15 | display: flex;
16 | }
--------------------------------------------------------------------------------
/packages/training-web-4/src/app/(static)/layout.tsx:
--------------------------------------------------------------------------------
1 | // 「3-2-2-2.Route Groups 専用のレイアウトをネストする」で解説されている「Layout B」ファイル
2 |
3 | import { Footer } from "../_components/Footer";
4 | import { Header } from "../_components/Header";
5 | import styles from "./layout.module.css";
6 |
7 | type Props = {
8 | children: React.ReactNode;
9 | };
10 |
11 | // 静的要素のみで構成される(static)配下で全適用されるレイアウト
12 | export default function Layout({ children }: Props) {
13 | return (
14 |
15 |
16 |
17 | {children}
18 |
19 |
20 |
21 | );
22 | }
23 |
--------------------------------------------------------------------------------
/packages/training-web-4/src/app/(static)/privacy-policy/page.tsx:
--------------------------------------------------------------------------------
1 | import { SITE_NAME } from "@/constants";
2 | import type { Metadata } from "next";
3 |
4 | export const metadata: Metadata = {
5 | title: `プライバシーポリシー | ${SITE_NAME}`,
6 | description: "運営企業「テックピクチャーズ株式会社」のプライバシーポリシー",
7 | };
8 |
9 | export default function Page() {
10 | return (
11 |
12 |
利用規約
13 |
14 | );
15 | }
16 |
--------------------------------------------------------------------------------
/packages/training-web-4/src/app/_components/Footer/index.tsx:
--------------------------------------------------------------------------------
1 | import Link from "next/link";
2 | import styles from "./style.module.css";
3 |
4 | export function Footer() {
5 | return (
6 |
16 | );
17 | }
18 |
--------------------------------------------------------------------------------
/packages/training-web-4/src/app/_components/Footer/style.module.css:
--------------------------------------------------------------------------------
1 | .footer {
2 | padding: 24px;
3 | border-top: 1px solid #000;
4 | }
--------------------------------------------------------------------------------
/packages/training-web-4/src/app/_components/Header/index.tsx:
--------------------------------------------------------------------------------
1 | import Link from "next/link";
2 | import styles from "./style.module.css";
3 |
4 | export function Header() {
5 | return (
6 |
7 |
8 | Photo Share
9 |
10 |
11 | );
12 | }
13 |
--------------------------------------------------------------------------------
/packages/training-web-4/src/app/_components/Header/style.module.css:
--------------------------------------------------------------------------------
1 | .header {
2 | padding: 24px;
3 | border-bottom: 1px solid #000;
4 | }
--------------------------------------------------------------------------------
/packages/training-web-4/src/app/api/photos/[photoId]/like/route.ts:
--------------------------------------------------------------------------------
1 | import type { NextRequest } from "next/server";
2 |
3 | export async function POST(
4 | _: NextRequest,
5 | { params }: { params: { photoId: string } },
6 | ) {
7 | console.log(`photoId ${params.photoId} が「いいね」されました`);
8 | // 🚧 TODO: 誰から送られたリクエストかを cookie から特定する処理
9 | // 🚧 TODO: DBサーバーなどに永続化するための処理
10 | return Response.json({ liked: true });
11 | }
12 |
--------------------------------------------------------------------------------
/packages/training-web-4/src/app/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/practical-nextjs-book/training/681cbae5ecb20ba9ab23c305bc0c358ddb2c3ae1/packages/training-web-4/src/app/favicon.ico
--------------------------------------------------------------------------------
/packages/training-web-4/src/app/global-error.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | export default function GlobalError({
4 | reset,
5 | }: {
6 | error: Error & { digest?: string };
7 | reset: () => void;
8 | }) {
9 | return (
10 |
11 |
12 | Something went wrong!
13 |
14 |
15 |
16 | );
17 | }
18 |
--------------------------------------------------------------------------------
/packages/training-web-4/src/app/layout.tsx:
--------------------------------------------------------------------------------
1 | import "../styles/globals.css";
2 | import { SITE_NAME } from "@/constants";
3 |
4 | export const metadata = {
5 | title: SITE_NAME,
6 | description:
7 | "「Photo Share」は、ユーザーが自由に写真を共有し、コメントや「いいね」を通じて交流することができるSNSアプリケーションです。",
8 | };
9 |
10 | export default async function RootLayout({
11 | children,
12 | }: {
13 | children: React.ReactNode;
14 | }) {
15 | return (
16 |
17 | {children}
18 |
19 | );
20 | }
21 |
--------------------------------------------------------------------------------
/packages/training-web-4/src/constants.ts:
--------------------------------------------------------------------------------
1 | export const SITE_NAME = "Photo Share";
2 |
--------------------------------------------------------------------------------
/packages/training-web-4/src/styles/globals.css:
--------------------------------------------------------------------------------
1 | * {
2 | box-sizing: border-box;
3 | margin: 0;
4 | padding: 0;
5 | font-family: 'Hiragino Kaku Gothic ProN', 'Arial', 'Helvetica', sans-serif;
6 | word-break: break-all;
7 | }
8 |
9 | html,
10 | body {
11 | max-width: 100vw;
12 | }
13 |
14 | a {
15 | color: inherit;
16 | color: blue;
17 | }
--------------------------------------------------------------------------------
/packages/training-web-4/src/type.ts:
--------------------------------------------------------------------------------
1 | export type Photo = {
2 | id: string;
3 | title: string;
4 | description: string;
5 | imageUrl: string;
6 | authorId: string;
7 | categoryId: string;
8 | };
9 |
10 | export type Category = {
11 | id: string;
12 | name: string;
13 | label: string;
14 | description: string;
15 | imageUrl: string;
16 | };
17 |
--------------------------------------------------------------------------------
/packages/training-web-4/src/utils/index.ts:
--------------------------------------------------------------------------------
1 | export function getPage(searchParams: {
2 | [key: string]: string | string[] | undefined;
3 | }) {
4 | if (typeof searchParams.page !== "string") return 1;
5 | const page = parseInt(searchParams.page);
6 | if (isNaN(page)) return 1;
7 | if (page < 1) return 1;
8 | return page;
9 | }
10 |
--------------------------------------------------------------------------------
/packages/training-web-4/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "lib": [
5 | "dom",
6 | "dom.iterable",
7 | "esnext"
8 | ],
9 | "allowJs": true,
10 | "skipLibCheck": true,
11 | "strict": true,
12 | "forceConsistentCasingInFileNames": true,
13 | "noEmit": true,
14 | "esModuleInterop": true,
15 | "module": "esnext",
16 | "moduleResolution": "node",
17 | "resolveJsonModule": true,
18 | "isolatedModules": true,
19 | "jsx": "preserve",
20 | "incremental": true,
21 | "plugins": [
22 | {
23 | "name": "next"
24 | }
25 | ],
26 | "paths": {
27 | "@/*": [
28 | "./src/*"
29 | ]
30 | }
31 | },
32 | "include": [
33 | "next-env.d.ts",
34 | "**/*.ts",
35 | "**/*.tsx",
36 | ".next/types/**/*.ts"
37 | ],
38 | "exclude": [
39 | "node_modules"
40 | ]
41 | }
42 |
--------------------------------------------------------------------------------