🎉

Nust3,spoonacularAPI,vercelを使って、超簡単な献立アプリを作ってみた。

2023/06/04に公開

背景

最近、PMとしての業務が増え、段々コーディングの機会が減っていますが、まだまだプログラマーとして成長したかったので、業務でも使用しているNuxt.jsの復習と学習内容を簡単にアウトプットできたらと思い、初めて投稿します。

内容

家で料理をする機会が多く、余った食材から献立を提示してくれるアプリあったら良いなと思ったので、spoonacularAPIを使った、超簡単な献立アプリを作ってみました。作成後、1分でデプロイできる、vercelさんにデプロイさせて頂きました。
(chatgptが全てを担ってくれる事は存じております。。。今回はあくまでも学習用です。。実用性はありません。。)

nuxtアプリを作成する

npx nuxi init kondate

今回使いそうな、パッケージを追加。今回は以下

  • cssフレームワークのtailwindcss
  • axios
  • dotenv
yarn add tailwindcss@latest postcss@latest autoprefixer@latest
yarn add axios
@nuxtjs/dotenv

tailwindの初期設定

npx tailwindcss init

上記のコマンドでtailwind.config.jsが作成されるので、ファイルに以下を記述

/** @type {import('tailwindcss').Config} */
module.exports = {
  content: [
    "./components/**/*.{js,vue,ts}",
    "./layouts/**/*.vue",
    "./pages/**/*.vue",
    "./plugins/**/*.{js,ts}",
    "./nuxt.config.{js,ts}",
    "./app.vue",
  ],
  theme: {
    extend: {},
  },
  plugins: [],
}

assets/css/main.cssを作成し、以下を記述

@tailwind base;
@tailwind components;
@tailwind utilities;

必要なディレクトリを作成する。今回はヘッダーを作成する際に必要な下記のlayout、componentとお馴染みpageディレクトリを作成

npx nuxi add layout default
npx nuxi add component navbar
npx nuxi add page index

一旦、最低限の必要な設定は終わったので、コーディングしていく。

components配下にヘッダーを作成。tailwindで適当にやっていきます。

components/navbar.vue
<script lang="ts" setup></script>

<template>
  <nav class="flex items-center justify-between flex-wrap bg-teal-500 p-6">
    <div class="container mx-auto flex justify-between items-center">
      <div class="flex items-center flex-shrink-0 text-white space-x-2">
        <img src="~/assets/nabe.png" alt="nabe" style="max-width: 30px" />
        <span class="font-semibold text-2xl tracking-tight">kondate</span>
      </div>
    </div>
  </nav>
</template>

<style scoped></style>

layout配下でnavbar.vueの内容をimportします。Nuxt3からcomponentsディレクトリ配下のファイルならimportの構文を書かなくてimportできるようになりました。

layouts/default.vue
<script lang="ts" setup></script>

<template>
  <div>
    <Navbar />
    <slot />
  </div>
</template>

<style scoped></style>

こんな感じです



tailwindcssの設定とruntimeConfigに環境変数の設定をしています。

nuxt.config.ts
export default defineNuxtConfig({
  css: ['~/assets/css/main.css'],
  postcss: {
    plugins: {
      tailwindcss: {},
      autoprefixer: {},
    },
  },
  runtimeConfig: {
    apiKey: '',
    public: {
      spoonApiKey: '',
    }
  },
});

環境変数について

envに下記を記載。NUXT_PUBLICの接頭辞をつければ、runtimeConfigの中で認識してくれるみたいです。publicはクライアントとサーバーサイドどちらも使えて、runtimeConfig直下であればprivateのみの認識になりサーバーサイドでしか使えない環境変数になるみたいです。

.env

NUXT_PUBLIC_SPOON_API_KEY=xxxxxxxxxxxxxxxxxxx

nuxt.config.ts
  runtimeConfig: {
    apiKey: '',
    public: {
      spoonApiKey: '',
    }
  },

メインのコードです。やってることは基本的な事しかしていないです。inputに入力されるデータ(食材)を双方向バインディングし、入力した内容をリストとして表示後、buttonをクリックするとaxiosでspoonacularAPIにリクエストし、レスポンスとして、献立を表示しています。
(spoonacularAPIが英語のみなので、現在、英語対応のみです。。。日本語入力⇨英語翻訳、リクエストも考えたのですが、今回は見送りました。)

pages/index.vue
<template>
  <div class="container mx-auto px-4 justify-center">
    <h1 class="text-2xl font-semibold text-center mt-4 mb-4">Add Ingredients</h1>
    <div class="flex items-center justify-center">
      <div class="space-x-4">
        <input type="text" v-model="newIngredients" placeholder="Ingredients" class="w-72 h-10 px-3 py-2 border border-gray-300 rounded" />
        <button @click="addIngredients" class="py-2 px-4 bg-blue-500 text-white rounded">Add Ingredients</button>
      </div>
    </div>
    <div class="flex justify-center mt-5">
      <ul class="list-disc">
        <li v-for="(Ingredient, index)  in Ingredients" :key=index class="flex space-x-4 items-center py-2">
          <span class="text-2xl"></span>
          <p class="text-2xl">{{ Ingredient }}</p>
          <button @click="removeIngredients(index)" class="text-1xl py-1 px-2 bg-red-500 text-white rounded">delete</button>
        </li>
      </ul>
    </div>
    <div class="flex justify-center mt-5" v-if="isTasksCount">
      <button @click="getRecipe()" class="py-2 px-4 bg-blue-500 text-white rounded">Find Recipes</button>
    </div>
    <div class="flex justify-center mt-5">
      <div class="flex space-x-4">
        <div v-for="recipe in recipes" :key="recipe.id" class="flex flex-col items-center">
          <img :src="recipe.image" class="w-72 h-72 object-cover" />
          <p class="text-2xl">{{ recipe.title }}</p>
        </div>
      </div>
    </div>
  </div>
</template>



<script setup lang="ts">
import { ref } from 'vue';
import axios from 'axios';

interface Recipe {
  id: number;
  title: string;
  image: string;
}

const Ingredients = ref<string[]>([]);
const recipes = ref<Recipe[]>([]);
const newIngredients = ref('');

const runtimeConfig = useRuntimeConfig();

const addIngredients = (): void => {
  if (newIngredients.value.trim() !== '') {
    const addition = Ingredients.value.length != 0 ? '+' : '';
    Ingredients.value.push(addition + newIngredients.value)
    newIngredients.value = '';
  }
}

const isTasksCount = computed ((): boolean => {
  return Ingredients.value.length > 0;
})

const removeIngredients = (index: number): void => {
  Ingredients.value.splice(index, 1);
}

const getRecipe = async (): Promise<void> => {
if (recipes.value.length !=0 ) recipes.value = [];
let res: any;
try {
  res = await axios.get('https://api.spoonacular.com/recipes/findByIngredients', {
    params: {
      apiKey: runtimeConfig.public.spoonApiKey,
      ingredients: Ingredients.value.join(','),
      number: 2,
    }
  });
} catch (error) {
  console.error('An error occurred while trying to fetch the data:', error);
}

if (res && res.data && res.data.length === 0) {
  alert('No recipe found.');
  Ingredients.value = [];
} else {
   Ingredients.value = [];
   res.data.forEach((recipe: Recipe) => {
     recipes.value.push({
       id: recipe.id,
       title: recipe.title,
       image: recipe.image,
     });
   });
}
}
</script>

<style scoped></style>




海外のAPIなので、あまり日本には馴染みのない料理が出てきました。。

vercelにデプロイ

https://vercel.com/

projectの作成とgithubからインポートして、deployをポチポチクリックしたらデプロイされます。1分でできます。

終わり

デプロイ後、URLが作成されるので、下記にアクセスすると無事動きました。
https://kondate-iywrpxxmb-ishinshibata.vercel.app/

今回はNuxt3を初めて触りましたが、まだ基本的なところしか触れていないので、今後、もう少し掘り下げて、また改めて投稿しようと思います。

Discussion