はじめに
この記事は前回記事の続きからになります。
全体の流れを把握したい場合は、前回記事からお読みください。
未経験者のためのNuxt 3、Prisma、MySQL入門!植物の水やり管理アプリを開発してみた①
追加する機能
前回記事の中では触れなかったメモ機能、水やり日の登録機能を実装する。
メモ機能の実装
基本的には前回実装した植物の名前を登録する処理と同じ。
<template>
<div class="container">
<h1>Top Page</h1>
<!-- PlantsModal -->
<div
class="modal fade"
id="exampleModal"
tabindex="-1"
aria-labelledby="exampleModalLabel"
aria-hidden="true"
>
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="exampleModalLabel">Edit Plant</h5>
<button
type="button"
class="btn-close"
data-bs-dismiss="modal"
aria-label="Close"
></button>
</div>
<div class="modal-body">
<input
v-model="editedPlant.name"
type="text"
class="form-control"
id="exampleInputName1"
aria-describedby="nameHelp"
/>
</div>
<div class="modal-footer">
<button
type="button"
class="btn btn-secondary"
data-bs-dismiss="modal"
>
Close
</button>
<button
type="button"
class="btn btn-primary"
data-bs-dismiss="modal"
@click="editPlant(editedPlant)"
>
Save changes
</button>
</div>
</div>
</div>
</div>
<div
v-if="error"
class="alert alert-danger alert-dismissible fade show"
role="alert"
>
<strong>Error:</strong> Delete Error
<button
type="button"
class="btn-close"
data-bs-dismiss="alert"
aria-label="Close"
@click="error = null"
></button>
</div>
<form>
<div class="mb-3">
<label for="exampleInputName1" class="form-label">Name</label>
<input
v-model="plant"
type="text"
class="form-control"
id="exampleInputName1"
aria-describedby="nameHelp"
/>
</div>
<button
type="submit"
class="btn btn-primary"
@click.prevent="addPlant(plant)"
>
Add Name
</button>
</form>
<table class="table">
<thead>
<tr>
<th scope="col">#</th>
<th scope="col">Name</th>
<th scope="col">Edit</th>
<th scope="col">Delete</th>
</tr>
</thead>
<tbody>
<tr v-for="(plant, index) in plants">
<th scope="row">{{ index + 1 }}</th>
<td>{{ plant.name }}</td>
<td>
<button
type="button"
class="btn btn-warning btn-sm"
data-bs-toggle="modal"
data-bs-target="#exampleModal"
@click="
{
editedPlant.id = plant.id;
editedPlant.name = plant.name;
}
"
>
Edit
</button>
</td>
<td>
<button
type="button"
class="btn btn-danger btn-sm"
@click="deletePlant(plant.id)"
>
Delete
</button>
</td>
</tr>
</tbody>
</table>
<!-- MemosModal 追加 -->
<div
class="modal fade"
id="exampleModal2"
tabindex="-1"
aria-labelledby="exampleModalLabel2"
aria-hidden="true"
>
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="exampleModalLabel2">Edit Memo</h5>
<button
type="button"
class="btn-close"
data-bs-dismiss="modal"
aria-label="Close"
></button>
</div>
<div class="modal-body">
<input
v-model="editedMemo.content"
type="text"
class="form-control"
id="exampleInputContent1"
aria-describedby="contentHelp"
/>
</div>
<div class="modal-footer">
<button
type="button"
class="btn btn-secondary"
data-bs-dismiss="modal"
>
Close
</button>
<button
type="button"
class="btn btn-primary"
@click="editMemo(editedMemo)"
data-bs-dismiss="modal"
>
Save changes
</button>
</div>
</div>
</div>
</div>
<form>
<div class="mb-3">
<label for="exampleInputMemo1" class="form-label">Memo</label>
<!-- textareaに変更 -->
<textarea
v-model="memo"
type="text"
class="form-control"
id="exampleInputMemo1"
aria-describedby="memoHelp"
/>
</div>
<button
type="submit"
class="btn btn-primary"
@click.prevent="addMemo(memo)"
>
Add Memo
</button>
</form>
<table class="table">
<thead>
<tr>
<th scope="col">#</th>
<th scope="col">Content</th>
<th scope="col">Edit</th>
<th scope="col">Delete</th>
<th scope="col">Date</th>
</tr>
</thead>
<tbody>
<tr v-for="(memo, index) in memos">
<th scope="row">{{ index + 1 }}</th>
<td>{{ memo.content }}</td>
<td>
<button
type="button"
class="btn btn-warning btn-sm"
data-bs-toggle="modal"
data-bs-target="#exampleModal2"
@click="
{
editedMemo.id = memo.id;
editedMemo.content = memo.content;
}
"
>
Edit
</button>
</td>
<td>
<button
type="button"
class="btn btn-danger btn-sm"
@click="deleteMemo(memo.id)"
>
Delete
</button>
</td>
<td>{{ memo.date }}</td>
</tr>
</tbody>
</table>
</div>
</template>
<script setup>
import { H3Error } from "h3";
const plants = ref(null);
const plant = ref(null);
const error = ref(null);
const editedPlant = ref({
id: null,
name: null,
});
// 追加
const memos = ref(null);
const memo = ref(null);
const editedMemo = ref({
id: null,
content: null,
});
plants.value = await getPlants();
memos.value = await getMemos(); // 追加
// Get plants
async function getPlants() {
return await $fetch("/api/plants");
}
// Add plant
async function addPlant(plant) {
let addedPlant = null;
if (plant)
addedPlant = await $fetch("/api/plants", {
method: "POST",
body: {
name: plant,
},
});
if (addedPlant) plants.value = await getPlants();
}
// Edit plant
async function editPlant(editedPlant) {
let plant = null;
if (editedPlant.id && editedPlant.name)
plant = await $fetch("/api/plants", {
method: "PUT",
body: {
id: editedPlant.id,
name: editedPlant.name,
},
});
if (plant) plants.value = await getPlants();
}
// Delete plant
async function deletePlant(id) {
let deletePlantOrError = null;
if (id)
deletePlantOrError = await $fetch("/api/plants", {
method: "DELETE",
body: {
id: id,
},
});
if (deletePlantOrError instanceof H3Error) {
error.value = deletePlantOrError;
return;
}
plants.value = await getPlants();
}
// Get memos 追加
async function getMemos() {
const fetchedMemos = await $fetch("/api/memos");
return fetchedMemos.map((memo) => ({
...memo,
date: new Date(memo.created_at).toLocaleDateString(),
}));
}
// Add memos 追加
async function addMemo(memo) {
let addedMemo = null;
if (memo)
addedMemo = await $fetch("/api/memos", {
method: "POST",
body: {
content: memo,
},
});
if (addedMemo) memos.value = await getMemos();
}
// Edit memo 追加
async function editMemo(editedMemo) {
let memo = null;
if (editedMemo.id && editedMemo.content)
memo = await $fetch("/api/memos", {
method: "PUT",
body: {
id: editedMemo.id,
content: editedMemo.content,
},
});
if (memo) memos.value = await getMemos();
}
// Delete memo 追加
async function deleteMemo(id) {
let deleteMemoOrError = null;
if (id)
deleteMemoOrError = await $fetch("/api/memos", {
method: "DELETE",
body: {
id: id,
},
});
if (deleteMemoOrError instanceof H3Error) {
error.value = deleteMemoOrError;
return;
}
memos.value = await getMemos();
}
useHead({
title: "Green Care",
link: {
rel: "stylesheet",
href: "https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css",
type: "text/css",
},
script: {
src: "https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/js/bootstrap.bundle.min.js",
},
});
</script>
model memos {
id Int @id @default(autoincrement())
content String
created_at DateTime @default(now()) @db.DateTime(0)
updated_at DateTime? @updatedAt
}
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
export default defineEventHandler(async (event) => {
const body = await readBody(event)
let memo = null
if (body.content)
await prisma.memos.create({
data: {
content: body.content,
},
}).then((response: any) => {
memo = response
})
return {
memo: memo
}
})
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
export default defineEventHandler(async () => {
return await prisma.memos.findMany()
})
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
export default defineEventHandler(async (event) => {
const body = await readBody(event)
const id = body.id
const content = body.content
if (!(id && content)) return createError({ statusCode: 400, statusMessage: "Missing id and content"});
let memo = null
if (id && content)
memo = await prisma.memos.update({
where: {
id: id,
},
data: {
content: content,
},
})
return memo
})
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
export default defineEventHandler(async (event) => {
const body = await readBody(event)
let memo = null
let error = null
if(body.id)
await prisma.memos.delete({
where: {
id: body.id,
},
}).then((response) => {
memo = response
}).catch(async(e) => {
error = e
})
if (error) return createError({ statusCode: 500, statusMessage: "Server error"});
return memo
})
解説
PlantsModalではinputタグだった入力欄をtextareaタグに変更。
async function getMemos() {
const fetchedMemos = await $fetch("/api/memos");
return fetchedMemos.map((memo) => ({
...memo,
date: new Date(memo.created_at).toLocaleDateString(),
}));
}
dateプロパティを追加し、memo.created_atをnew Dateを使用して日付オブジェクトに変換し、toLocaleDateStringメソッドを呼び出してローカルの日付表現に変換する。
<td>{{ memo.date }}</td>
そして、上記のように記述し、メモ追加日の日付を表示する。
水やり日の登録機能の実装
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
export default defineEventHandler(async (event) => {
const body = await readBody(event);
const id = body.id;
const name = body.name;
const lastWateredAt = body.lastWatered_at; // 追加
if (!(id && name)) return createError({ statusCode: 400, statusMessage: "Missing id and name"});
let plant = null;
if (id && name) {
plant = await prisma.plants.update({
where: {
id: id,
},
data: {
name: name,
lastWatered_at: lastWateredAt, //追加
},
});
}
return plant;
});
plants.put.tsにコードを追加。
<table class="table">
<thead>
<tr>
<th scope="col">#</th>
<th scope="col">Name</th>
<th scope="col">水やり</th>
<th scope="col">前回の水やり日</th>
<th scope="col">Edit</th>
<th scope="col">Delete</th>
</tr>
</thead>
<tbody>
<tr v-for="(plant, index) in plants">
<th scope="row">{{ index + 1 }}</th>
<td>{{ plant.name }}</td>
<!-- 追加 -->
<td>
<button
type="button"
class="btn btn-info"
@click="markAsWatered(plant)"
>
Done
</button>
</td>
<td>
<p v-if="plant.lastWatered_at">
{{ new Date(plant.lastWatered_at).toLocaleDateString() }}
</p>
<p v-else></p>
</td>
<td>
<button
type="button"
class="btn btn-warning btn-sm"
data-bs-toggle="modal"
data-bs-target="#exampleModal"
@click="
{
editedPlant.id = plant.id;
editedPlant.name = plant.name;
}
"
>
Edit
</button>
</td>
<td>
<button
type="button"
class="btn btn-danger btn-sm"
@click="deletePlant(plant.id)"
>
Delete
</button>
</td>
</tr>
</tbody>
</table>
tableに水やり、前回の水やり日の項目を追加。
Doneにクリックイベントを追加し、ボタンクリック時に日付を表示させる処理を記述。
<script setup>
import { H3Error } from "h3";
const plants = ref(null);
const plant = ref(null);
const error = ref(null);
const editedPlant = ref({
id: null,
name: null,
});
const memos = ref(null);
const memo = ref(null);
const editedMemo = ref({
id: null,
content: null,
});
plants.value = await getPlants();
memos.value = await getMemos();
// Get plants
async function getPlants() {
return await $fetch("/api/plants");
}
// Add plant
async function addPlant(plant) {
let addedPlant = null;
if (plant)
addedPlant = await $fetch("/api/plants", {
method: "POST",
body: {
name: plant,
},
});
if (addedPlant) plants.value = await getPlants();
}
// Edit plant
async function editPlant(editedPlant) {
let plant = null;
if (editedPlant.id && editedPlant.name)
plant = await $fetch("/api/plants", {
method: "PUT",
body: {
id: editedPlant.id,
name: editedPlant.name,
},
});
if (plant) plants.value = await getPlants();
}
// Delete plant
async function deletePlant(id) {
let deletePlantOrError = null;
if (id)
deletePlantOrError = await $fetch("/api/plants", {
method: "DELETE",
body: {
id: id,
},
});
if (deletePlantOrError instanceof H3Error) {
error.value = deletePlantOrError;
return;
}
plants.value = await getPlants();
}
// Get memos
async function getMemos() {
const fetchedMemos = await $fetch("/api/memos");
return fetchedMemos.map((memo) => ({
...memo,
date: new Date(memo.created_at).toLocaleDateString(),
}));
}
// Add memos
async function addMemo(memo) {
let addedMemo = null;
if (memo)
addedMemo = await $fetch("/api/memos", {
method: "POST",
body: {
content: memo,
},
});
if (addedMemo) memos.value = await getMemos();
}
// Edit memo
async function editMemo(editedMemo) {
let memo = null;
if (editedMemo.id && editedMemo.content)
memo = await $fetch("/api/memos", {
method: "PUT",
body: {
id: editedMemo.id,
content: editedMemo.content,
},
});
if (memo) memos.value = await getMemos();
}
// Delete memo
async function deleteMemo(id) {
let deleteMemoOrError = null;
if (id)
deleteMemoOrError = await $fetch("/api/memos", {
method: "DELETE",
body: {
id: id,
},
});
if (deleteMemoOrError instanceof H3Error) {
error.value = deleteMemoOrError;
return;
}
memos.value = await getMemos();
}
// 追加
async function markAsWatered(plant) {
const now = new Date().toISOString();
const updatedPlant = await $fetch("/api/plants", {
method: "PUT",
body: {
id: plant.id,
name: plant.name,
lastWatered_at: now,
},
});
if (updatedPlant) {
plants.value = await getPlants();
}
}
useHead({
title: "Green Care",
link: {
rel: "stylesheet",
href: "https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css",
type: "text/css",
},
script: {
src: "https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/js/bootstrap.bundle.min.js",
},
});
</script>
解説
const now = new Date().toISOString()
現在の日時を取得し、ISO 8601形式で文字列化。
$fetch("/api/plants", { ... })
サーバーの/api/plantsエンドポイントに対して、HTTPメソッドがPUTのリクエストを送信。
リクエストボディには、植物のID、名前、現在日時を含む情報が含まれている。
if (updatedPlant) {
plants.value = await getPlants();
}
もし更新が成功した場合、getPlants関数を呼び出して最新の植物情報を取得し、その結果をplants.valueに格納する。
plants変数に最新の植物情報を設定することで、UIが更新される。
デプロイ
NuxtはVercel、データベースはPlanetScaleにデプロイ。
Deploy to Vercel - PlanetScale Documentation
上記のドキュメントの手順を踏まえてデプロイしたが、エラーが発生。
改めて下記の公式ドキュメントをよく読み、package.jsonに追記することで無事デプロイ完了!(このエラーの解決に2日溶けた)
Deploy to Vercel - Prisma
公式ドキュメントを読もう...!
今後の課題
このままでは誰でもデータを変更できてしまう状態に...
知識不足のため今回はその対処ができなかったため次回はそこに着手したい。
ログイン機能が必要なのか、または他に制限する方法があるのか調べてみる。
まとめ
Nuxt3の基礎学習を終えて実際にアプリを作ってみたが、何かを形にしていくのはやはり楽しい。
学んだ箇所がどのように使われていくか基礎学習の段階ではイメージがしにくいため、どんどん作ることが大切だと感じた。
基礎学習をしていても、...でこの処理はアプリではどうやって使われるの?の連続。
そうしていくうちに学習中に飽きがきてしまう。
そのような兆候を感じたら一刻も早く何かを作ることを今後も意識したい。
作りながら基礎部分を調べると理解しやすいことが多く、基礎学習自体が目的とならないよう気をつけていきたい。