はじめに
前回の買い物リストアプリの作成の際に、改良事項として下記にまとめました。
・ヘッダーをつけて、横にカートをおしてる女性を白抜きで表示
・日付横に時刻まで入れて今の時間だとタイムセールかも??みたいな風に
・くすみグリーンの色合いで表示をしたいが、まっしろなまま動かず
今回はこれを参考に改良をしていきます。
作成過程
前回の内容がフロントエンド寄りの修正内容だったので、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>
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')
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への公開も視野に入れて修正をしていきます。