├── .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 |
7 |

8 | {/* ★:SPAナビゲーションを提供する Link コンポーネント */} 9 | Photo Share 10 |

11 |
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 | 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 |
22 |
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 | 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 | 29 | 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 | 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 |
22 |
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 | 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 | 24 | 25 | 26 |
概要概要テキスト
カテゴリー 22 | 花 23 |
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 | 47 | 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 | 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 |
22 |
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 | 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 | 35 | 36 | 37 | 38 | 43 | 44 | 45 |
概要{photo.description}
カテゴリー 39 | 40 | {category.label} 41 | 42 |
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 | 69 | 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 | 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 |
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 | 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 | 44 | 45 | 46 | 47 | 52 | 53 | 54 |
概要{photo.description}
カテゴリー 48 | 49 | {category.label} 50 | 51 |
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 |
7 |
    8 |
  • 9 | プライバシー・ポリシー 10 |
  • 11 |
  • 12 | 運営企業 13 |
  • 14 |
15 |
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 | --------------------------------------------------------------------------------