開発サーバーを立てるスクリプト npm run dev
は差分ビルドをおこなうので npm run build
は不要です
ようするにとりあえず開発するのであれば npm run dev
しておけば大丈夫です
.env
を編集し環境変数の内容を変更した場合は Ctrl + C を入力して開発サーバーを停止・再起動してください(変更前の .env
ファイルが読まれ続けるため)
環境変数の値は useRuntimeConfig() に定義し参照する方針を採用したので、それに倣ってください
参考: https://nuxt.com/docs/api/composables/use-runtime-config#useruntimeconfig
@aws-sdk/client-s3 の使い方が載っているドキュメント
- S3 Client - AWS SDK for JavaScript v3
- Amazon S3 examples using SDK for JavaScript (v3) - AWS SDK for JavaScript
削除系 API エンドポイントを利用しないかぎりポケモンは保持する差分
--- a/server/utils/router.js
+++ b/server/utils/router.js
@@ -53,10 +65,23 @@ router.post("/trainer/:trainerName", async (req, res, next) => {
router.post("/trainer/:trainerName/pokemon", async (req, res, next) => {
try {
const { trainerName } = req.params;
- // TODO: リクエストボディにポケモン名が含まれていなければ400を返す
+ const trainer = await findTrainer(trainerName); // 先にトレーナーを取得する処理を実装する必要があります
+ if (!("name" in req.body && req.body.name.length > 0))
+ return res.sendStatus(400);
const pokemon = await findPokemon(req.body.name);
- // TODO: 削除系 API エンドポイントを利用しないかぎりポケモンは保持する
- const result = await upsertTrainer(trainerName, { pokemons: [pokemon] });
+ const {
+ order,
+ name,
+ sprites: { front_default },
+ } = pokemon;
+ trainer.pokemons.push({
+ id: new Date().getTime(), // 何か衝突しない値の生成方法であればなんでもいいです
+ });
+ const result = await upsertTrainer(trainerName, trainer);
res.status(result["$metadata"].httpStatusCode).send(result);
} catch (err) {
next(err);
トレーナーがいなければ「つづきからはじめる」に遷移不可能にする差分
--- a/pages/index.vue
+++ b/pages/index.vue
@@ -1,12 +1,17 @@
-<script setup></script>
+<script setup>
+const { data: trainers } = await useTrainers();
+</script>
<template>
<div>
<h1>ポケットモンスター</h1>
<GamifyList>
- <GamifyItem>
+ <GamifyItem v-if="trainers.length > 0">
<NuxtLink to="/trainer">つづきからはじめる</NuxtLink>
</GamifyItem>
+ <GamifyItem v-else>
+ <span>つづきからはじめる</span>
+ </GamifyItem>
<GamifyItem>
<NuxtLink to="/new">あたらしくはじめる</NuxtLink>
</GamifyItem>
Nuxt における動的なルーティングの提供方法の和訳
かぎ括弧で囲った中に何かを記述した場合、その部分は動的ルートパラメーターに変換されます。 ファイル名かディレクトリに対して、複数のパラメーターもしくは静的なテキストを組み合わせることができます。 (省略)
-| pages/ ---| index.vue ---| users-[group]/ -----| [id].vue
上記の例では、
$route
オブジェクトから group / id にアクセスすることができます<template> {{ $route.params.group }} {{ $route.params.id }} </template>
/users-admins/123
に移動すると以下のように表示されますadmins 123
https://nuxt.com/docs/guide/directory-structure/pages#dynamic-routes
フロントエンド側から用意したサーバー API を取得するコードサンプル
useFetch を使う場合
const config = useRuntimeConfig();
// data: リアクティブなレスポンスボディ
// refresh: 再読み込みする関数
const { data, refresh } = useFetch("$/api/trainers", {
server: false, // ブラウサ側でのみデータ取得する(単純な実装にしておく目的)
baseURL: config.public.backendOrigin, // `npm run dev:express` しないなら省略可
});
// 動的な URL に対しては文字列を返す関数を引数に渡します
const { data, refresh } = useFetch(() => `/api/trainer/${trainerName}`, {
server: false, // ブラウサ側でのみデータ取得する(単純な実装にしておく目的)
baseURL: config.public.backendOrigin, // `npm run dev:express` しないなら省略可
});
参考: https://nuxt.com/docs/getting-started/data-fetching
トレーナー名をリクエストボディに含めてトレーナー追加 API を叩く差分
--- a/pages/new.vue
+++ b/pages/new.vue
@@ -1,4 +1,22 @@
-<script setup></script>
+<script setup>
+const router = useRouter();
+const config = useRuntimeConfig();
+const trainerName = ref("");
+const onSubmit = async () => {
+ const response = await $fetch("/api/trainer", {
+ baseURL: config.public.backendOrigin, // `npm run dev:express` しないなら省略可
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({
+ name: trainerName.value,
+ }),
+ }).catch((e) => e); // 正常でないレスポンスステータスやネットワークエラーはエラーが投げられるので取得結果に含める https://github.com/unjs/ofetch#%EF%B8%8F-handling-errors
+ if (response instanceof Error) return; // 取得結果がエラークラスインスタンスなら後続の処理をスキップする
+ router.push(`/trainer/${trainerName.value}`);
+};
+</script>
<template>
<div>
フロントエンド側でトレーナー名を入力しているかバリデーション(検証)する差分
--- a/pages/new.vue
+++ b/pages/new.vue
@@ -1,4 +1,7 @@
-<script setup></script>
+<script setup>
+const trainerName = ref("");
+const valid = computed(() => trainerName.value.length > 0);
+</script>
<template>
<div>
トレーナー名から S3 オブジェクトキーとしての使用を避けたい文字を取り除く差分
trimAvoidCharacters という関数がどこかに定義済みなので使用可能です。
--- a/pages/new.vue
+++ b/pages/new.vue
@@ -1,4 +1,7 @@
-<script setup></script>
+<script setup>
+const trainerName = ref("");
+const safeTranerName = computed(() => trimAvoidCharacters(trainerName.value));
+</script>
<template>
<div>
---
## ヒント 10
トレーナーを追加する入力フォームの差分
```diff
--- a/pages/new.vue
+++ b/pages/new.vue
@@ -3,7 +3,21 @@
<template>
<div>
<h1>あたらしくはじめる</h1>
- <form @submit.prevent></form>
+ <p>では はじめに きみの なまえを おしえて もらおう!</p>
+ <form @submit.prevent>
+ <div class="item">
+ <label for="name">なまえ</label>
+ <span id="name-description"
+ >とくていの もじは とりのぞかれるぞ!</span
+ >
+ <input
+ id="name"
+ v-model="trainerName"
+ aria-decribedby="name-description"
+ @keydown.enter="onSubmit"
+ />
+ <GamifyButton type="button" @click="onSubmit">けってい</GamifyButton>
+ </div>
+ </form>
</div>
</template>
ダイアログを追加する差分
--- a/pages/new.vue
+++ b/pages/new.vue
@@ -1,9 +1,29 @@
-<script setup></script>
+<script setup>
+const trainerName = ref("");
+const { dialog, onOpen, onClose } = useDialog();
+</script>
<template>
<div>
<h1>あたらしくはじめる</h1>
<form @submit.prevent></form>
+ <GamifyButton @click="onOpen(true)">ダイアログをひらく</GamifyButton>
+ <GamifyDialog
+ v-if="dialog"
+ id="confirm-submit"
+ title="かくにん"
+ :description="`ふむ・・・ きみは ${trainerName} と いうんだな!`"
+ @close="onClose"
+ >
+ <GamifyList :border="false" direction="horizon">
+ <GamifyItem>
+ <GamifyButton @click="onClose">いいえ</GamifyButton>
+ </GamifyItem>
+ <GamifyItem>
+ <GamifyButton @click="onClose">はい</GamifyButton>
+ </GamifyItem>
+ </GamifyList>
+ </GamifyDialog>
</div>
</template>