0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【WEBアプリ開発】買い物リストアプリ②

Posted at

はじめに

前回の買い物リストアプリの作成の際に、改良事項として下記にまとめました。
・ヘッダーをつけて、横にカートをおしてる女性を白抜きで表示
・日付横に時刻まで入れて今の時間だとタイムセールかも??みたいな風に
・くすみグリーンの色合いで表示をしたいが、まっしろなまま動かず
今回はこれを参考に改良をしていきます。

作成過程

前回の内容がフロントエンド寄りの修正内容だったので、vue周りの修正を行っています。

App.Vue
<template>
-  <div class="flex flex-col min-h-screen">
+  <div class="d-flex flex-column min-vh-100 bg-light">

-    <header class="bg-green-200 text-gray-800 p-4 flex justify-between items-center">
+    <header class="p-3 d-flex justify-content-between align-items-center shadow-sm fixed-top-header"
+            style="background-color: #79a38f; color: #fff;">
-      <div class="flex items-center space-x-2">
-        <span class="text-xl">🛒</span>
-        <h1 class="text-xl font-bold">Shopping-ListApp</h1>
+      <div class="d-flex align-items-center gap-2">
+        <span class="fs-3">🛍️</span>
+        <h1 class="fs-4 fw-bold mb-0">My Shopping List</h1>
       </div>
-      <div>{{ currentDate }}</div>
+      <div class="text-white small text-end">
+        {{ currentDate }}<br>{{ currentTime }}
+      </div>
     </header>

-    <div class="flex flex-1">
-      <aside class="w-48 bg-gray-800 text-white p-4 space-y-4">
-        <button class="w-full bg-gray-700 hover:bg-gray-600 p-2 rounded">Settings</button>
-        <button class="w-full bg-gray-700 hover:bg-gray-600 p-2 rounded">Search</button>
-        <button class="w-full bg-gray-700 hover:bg-gray-600 p-2 rounded">Add Item</button>
-      </aside>
-
-      <main class="flex-1 p-6 bg-white">
-        <div class="mb-4">
-          <input v-model="newItem.name" placeholder="商品名" class="border p-2 mr-2" />
-          <input v-model.number="newItem.quantity" type="number" placeholder="数量" class="border p-2 mr-2 w-20" />
-          <select v-model="newItem.category" class="border p-2 mr-2">
-            <option value="Food">Food</option>
-            <option value="Household">Household</option>
-          </select>
-          <button @click="addItem" class="bg-green-500 text-white px-4 py-2 rounded">追加</button>
-        </div>

-        <h1 class="text-2xl font-bold mb-4">買い物リスト</h1>
-        <ul>
-          <li v-for="item in items" :key="item.id"
-              class="mb-2 p-2 border rounded flex justify-between items-center">
-            <span>🛒 {{ item.name }}{{ item.quantity }}</span>
-            <button @click="deleteItem(item.id)" class="bg-red-500 text-white px-2 py-1 rounded ml-4">🗑 削除</button>
-          </li>
-        </ul>
-      </main>
+    <main class="flex-grow-1 p-4 bg-white mt-fixed-header mb-fixed-footer main-content-full-width">
+      <h2 class="fw-bold mb-4 d-flex align-items-center gap-2">
+        <i class="bi bi-cart-fill"></i> 買い物リスト
+      </h2>
+
+      <div v-if="items.length === 0" class="alert alert-info text-center" role="alert">
+        リストに商品がありません。下のフォームから追加しましょう!
+      </div>
+
+      <ul class="list-group shadow-sm">
+        <li v-for="item in items" :key="item.id"
+            class="list-group-item d-flex justify-content-between align-items-center p-3 mb-2 rounded border-0"
+            :class="{'list-group-item-success': item.category === 'Food', 'list-group-item-primary': item.category === 'Household', 'list-group-item-info': item.category === 'Other'}">
+          <div class="d-flex align-items-center flex-grow-1 me-3">
+            <span class="me-3 fs-5 flex-shrink-0">{{ getCategoryIcon(item.category) }}</span>
+            <div class="d-flex flex-column flex-grow-1">
+              <span class="fw-bold text-wrap text-break">{{ item.name }}</span>
+              <span class="badge bg-secondary rounded-pill mt-1 align-self-start">{{ item.quantity }}</span>
+            </div>
+          </div>
+          <button @click="deleteItem(item.id)" class="btn btn-danger btn-sm flex-shrink-0">
+            <i class="bi bi-trash-fill"></i> 削除
+          </button>
+        </li>
+      </ul>
+    </main>

+    <footer class="fixed-bottom-footer p-3 bg-dark text-white shadow-lg">
+      <div class="footer-content-wrapper d-flex flex-column gap-2">
+        <input v-model="newItem.name" type="text" class="form-control form-control-lg" placeholder="商品名を入力" />
+        <div class="d-flex gap-2">
+          <input v-model.number="newItem.quantity" type="number" class="form-control form-control-lg w-25" placeholder="数量" min="1" />
+          <select v-model="newItem.category" class="form-select form-select-lg flex-grow-1">
+            <option value="Food">食料品</option>
+            <option value="Household">日用品</option>
+            <option value="Other">その他</option>
+          </select>
+        </div>
+        <button @click="addItem" class="btn btn-success btn-lg mt-2">
+          <i class="bi bi-plus-circle-fill"></i> リストに追加
+        </button>
+        <button class="btn btn-outline-light text-start w-100 mt-2" @click="alert('設定はまだ未実装です')">
+          <i class="bi bi-gear-fill me-2"></i> 設定
+        </button>
+      </div>
+    </footer>
   </div>
</template>

<script>
 import api from './api'

 export default {
   data() {
     return {
       items: [],
       newItem: {
         name: '',
         quantity: 1,
         category: 'Food'
       },
-      currentDate: new Date().toLocaleDateString()
+      currentDate: new Date().toLocaleDateString('ja-JP'),
+      currentTime: new Date().toLocaleTimeString('ja-JP', { hour: '2-digit', minute: '2-digit' })
     }
   },
   async created() {
     const response = await api.getItems()
     this.items = response.data
   },
+  mounted() {
+    this.timer = setInterval(() => {
+      this.currentTime = new Date().toLocaleTimeString('ja-JP', { hour: '2-digit', minute: '2-digit' });
+    }, 1000);
+  },
+  beforeUnmount() {
+    clearInterval(this.timer);
+  },
   methods: {
     async addItem() {
       if (!this.newItem.name.trim()) return
       const response = await api.addItem(this.newItem)
       this.items.push(response.data)
       this.newItem = { name: '', quantity: 1, category: 'Food' }
     },
     async deleteItem(id) {
       await api.deleteItem(id)
       this.items = this.items.filter(item => item.id !== id)
     },
+    getCategoryIcon(category) {
+      switch (category) {
+        case 'Food': return '🍎';
+        case 'Household': return '🏠';
+        case 'Other': return '📦';
+        default: return '🛒';
+      }
+    }
   }
 }
</script>

<style scoped>
/* くすみグリーンの調整やロゴ用 */
.logo {
  display: block;
  margin: 0 auto 2rem;
}

@media (min-width: 1024px) {
  header {
    display: flex;
    place-items: center;
    padding-right: calc(var(--section-gap) / 2);
  }

  .logo {
    margin: 0 2rem 0 0;
  }

  header .wrapper {
    display: flex;
    place-items: flex-start;
    flex-wrap: wrap;
  }
}

+ body, html {
+   margin: 0;
+   padding: 0;
+   min-height: 100%;
+   width: 100%;
+   overflow-x: hidden;
+   box-sizing: border-box;
+ }
+ *, *::before, *::after {
+   box-sizing: border-box;
+ }
+ .bg-light {
+   background-color: #f8f9fa !important;
+ }
+ .fixed-top-header {
+   position: fixed;
+   top: 0;
+   left: 0;
+   width: 100%;
+   z-index: 1030;
+ }
+ .fixed-bottom-footer {
+   position: fixed;
+   bottom: 0;
+   left: 0;
+   width: 100%;
+   z-index: 1030;
+   display: flex;
+   justify-content: center;
+ }
+ .footer-content-wrapper {
+   max-width: 420px;
+   width: 100%;
+ }
+ .mt-fixed-header {
+   margin-top: 75px;
+ }
+ .mb-fixed-footer {
+   margin-bottom: 250px;
+ }
+ .main-content-full-width {
+   width: 100%;
+   flex-basis: auto;
+   min-width: 0;
+ }
+ .form-control-lg, .form-select-lg, .btn-lg {
+   height: calc(2.8rem + 2px);
+   font-size: 1.1rem;
+ }
+ .list-group-item {
+   border-radius: 0.5rem !important;
+   margin-bottom: 0.75rem !important;
+   border: 1px solid rgba(0, 0, 0, 0.05) !important;
+ }
+ .list-group-item-success {
+   background-color: #d1e7dd;
+   color: #0f5132;
+ }
+ .list-group-item-primary {
+   background-color: #cfe2ff;
+   color: #052c65;
+ }
+ .list-group-item-info {
+   background-color: #cff4fc;
+   color: #055160;
+ }
+ .list-group-item .fw-bold {
+   word-wrap: break-word;
+   word-break: break-all;
+ }
+ .badge {
+   font-size: 0.9em;
+   padding: 0.4em 0.8em;
+ }
+ ::-webkit-scrollbar {
+   width: 0;
+   background: transparent;
+ }

</style>

main.js
import './assets/main.css'

import { createApp } from 'vue'
import App from './App.vue'

// Bootstrap を追加!
import 'bootstrap/dist/css/bootstrap.min.css'
import 'bootstrap'

createApp(App).mount('#app')

:point_up:UI/デザインの変更
・Tailwind CSS ベース→ Bootstrap 5 + Icons ベースに移行。

Bootstrap 5
CSSフレームワークの一つで、知識がなくても使える/デザイン性の確保/レスポンシブWEBデザインが特徴。
・Angular directives for Bootstrap:AngularJSと連携可能
・Twitter Bootstrap:Twitter社が提供
・Bootstrap Themes:Bootstrap公式。無料・有料の区分あり。
・BootstrapWP:WordPress用にカスタマイズ済
現在はこの4種がある。

Icons

・全体背景色を bg-light(薄いグレー)に変更。

ヘッダー:
・固定 (fixed-top-header)
・背景色をくすんだ緑系 #79a38f に変更
・アイコンを 🛒 から 🛍️ に変更
・アプリ名を「Shopping-ListApp」→「My Shopping List」に変更
・日付に加え、リアルタイムの時刻表示を追加
・サイドバーを廃止(左カラムなし)
・リストは ul class="list-group"形式でカテゴリに応じて色分け表示(Food / Household / Other)
・商品がない時のメッセージ(alert-info)を追加
・入力フォームをフッターに移動し、スマホで使いやすいよう調整 (fixed-bottom-footer)
・Bootstrapアイコン使用(設定ボタンや追加ボタンなど)

機能面の変更
・商品名入力が空欄の場合、alertで通知(バリデーション追加)
・商品削除時に confirm('本当に削除しますか?') で確認ダイアログ表示
・カテゴリが "Other" の選択肢を追加
・商品カテゴリに応じたアイコン表示(🍎🏠📦など)

時刻表示の追加
・currentTime を mounted() 内で1秒ごとに更新(setInterval)
・beforeUnmount() で clearInterval() によりメモリリークを防止

エラーハンドリング
・API呼び出し失敗時に console.error + alert() を表示する処理を追加(addItem, deleteItem, created())

スタイル変更(CSS)
・スクロールバー非表示設定を追加(::-webkit-scrollbar)
・box-sizing: border-box を全要素に適用
・form-control-lg, btn-lg などのサイズ調整
・ヘッダーとフッターの高さを考慮した margin-top, margin-bottom 設定でレイアウト崩れを防止
・.main-content-full-width, .footer-content-wrapper などのクラスでスマホ対応強化

今後の課題

・DBにつなぐ
・機能の拡張(表示切替の際のエフェクトの追加、買い物リストに合うレシピ予測など)

おわりに

今回はフロントエンド周りの修正を行いました。
Boostrapは某ゲームにも使用されていたことを知り、ぜひ一度使ってみたいと思ったので、今回は非常にいい機会だったなと思います。大分静かめのデザインになっているので、次はド派手なデザインの際はぜひ使ってみようかなと。
今後はDBへつなぎ、gitへの公開も視野に入れて修正をしていきます。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?