瀏覽代碼

feat: add cart/order feature

bregsiaju 1 年之前
父節點
當前提交
293aabf709
共有 10 個檔案被更改,包括 254 行新增63 行删除
  1. 68
    0
      package-lock.json
  2. 1
    0
      package.json
  3. 22
    0
      public/categories.json
  4. 12
    20
      src/components/CardMenu.vue
  5. 17
    6
      src/components/CartItem.vue
  6. 11
    20
      src/components/MenuContainer.vue
  7. 31
    17
      src/components/OrderList.vue
  8. 6
    0
      src/main.js
  9. 59
    0
      src/stores/cart.js
  10. 27
    0
      src/stores/menus.js

+ 68
- 0
package-lock.json 查看文件

@@ -11,6 +11,7 @@
11 11
         "@popperjs/core": "^2.11.8",
12 12
         "bootstrap": "^5.3.1",
13 13
         "jquery": "^3.7.1",
14
+        "pinia": "^2.1.6",
14 15
         "vue": "^3.3.4",
15 16
         "vue-router": "^4.2.4"
16 17
       },
@@ -2164,6 +2165,56 @@
2164 2165
         "url": "https://github.com/sponsors/jonschlinkert"
2165 2166
       }
2166 2167
     },
2168
+    "node_modules/pinia": {
2169
+      "version": "2.1.6",
2170
+      "resolved": "https://registry.npmjs.org/pinia/-/pinia-2.1.6.tgz",
2171
+      "integrity": "sha512-bIU6QuE5qZviMmct5XwCesXelb5VavdOWKWaB17ggk++NUwQWWbP5YnsONTk3b752QkW9sACiR81rorpeOMSvQ==",
2172
+      "dependencies": {
2173
+        "@vue/devtools-api": "^6.5.0",
2174
+        "vue-demi": ">=0.14.5"
2175
+      },
2176
+      "funding": {
2177
+        "url": "https://github.com/sponsors/posva"
2178
+      },
2179
+      "peerDependencies": {
2180
+        "@vue/composition-api": "^1.4.0",
2181
+        "typescript": ">=4.4.4",
2182
+        "vue": "^2.6.14 || ^3.3.0"
2183
+      },
2184
+      "peerDependenciesMeta": {
2185
+        "@vue/composition-api": {
2186
+          "optional": true
2187
+        },
2188
+        "typescript": {
2189
+          "optional": true
2190
+        }
2191
+      }
2192
+    },
2193
+    "node_modules/pinia/node_modules/vue-demi": {
2194
+      "version": "0.14.6",
2195
+      "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.6.tgz",
2196
+      "integrity": "sha512-8QA7wrYSHKaYgUxDA5ZC24w+eHm3sYCbp0EzcDwKqN3p6HqtTCGR/GVsPyZW92unff4UlcSh++lmqDWN3ZIq4w==",
2197
+      "hasInstallScript": true,
2198
+      "bin": {
2199
+        "vue-demi-fix": "bin/vue-demi-fix.js",
2200
+        "vue-demi-switch": "bin/vue-demi-switch.js"
2201
+      },
2202
+      "engines": {
2203
+        "node": ">=12"
2204
+      },
2205
+      "funding": {
2206
+        "url": "https://github.com/sponsors/antfu"
2207
+      },
2208
+      "peerDependencies": {
2209
+        "@vue/composition-api": "^1.0.0-rc.1",
2210
+        "vue": "^3.0.0-0 || ^2.6.0"
2211
+      },
2212
+      "peerDependenciesMeta": {
2213
+        "@vue/composition-api": {
2214
+          "optional": true
2215
+        }
2216
+      }
2217
+    },
2167 2218
     "node_modules/postcss": {
2168 2219
       "version": "8.4.29",
2169 2220
       "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.29.tgz",
@@ -4269,6 +4320,23 @@
4269 4320
       "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
4270 4321
       "dev": true
4271 4322
     },
4323
+    "pinia": {
4324
+      "version": "2.1.6",
4325
+      "resolved": "https://registry.npmjs.org/pinia/-/pinia-2.1.6.tgz",
4326
+      "integrity": "sha512-bIU6QuE5qZviMmct5XwCesXelb5VavdOWKWaB17ggk++NUwQWWbP5YnsONTk3b752QkW9sACiR81rorpeOMSvQ==",
4327
+      "requires": {
4328
+        "@vue/devtools-api": "^6.5.0",
4329
+        "vue-demi": ">=0.14.5"
4330
+      },
4331
+      "dependencies": {
4332
+        "vue-demi": {
4333
+          "version": "0.14.6",
4334
+          "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.6.tgz",
4335
+          "integrity": "sha512-8QA7wrYSHKaYgUxDA5ZC24w+eHm3sYCbp0EzcDwKqN3p6HqtTCGR/GVsPyZW92unff4UlcSh++lmqDWN3ZIq4w==",
4336
+          "requires": {}
4337
+        }
4338
+      }
4339
+    },
4272 4340
     "postcss": {
4273 4341
       "version": "8.4.29",
4274 4342
       "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.29.tgz",

+ 1
- 0
package.json 查看文件

@@ -13,6 +13,7 @@
13 13
     "@popperjs/core": "^2.11.8",
14 14
     "bootstrap": "^5.3.1",
15 15
     "jquery": "^3.7.1",
16
+    "pinia": "^2.1.6",
16 17
     "vue": "^3.3.4",
17 18
     "vue-router": "^4.2.4"
18 19
   },

+ 22
- 0
public/categories.json 查看文件

@@ -0,0 +1,22 @@
1
+[
2
+  {
3
+    "id": 1,
4
+    "name": "Coffee"
5
+  },
6
+  {
7
+    "id": 2,
8
+    "name": "Tea"
9
+  },
10
+  {
11
+    "id": 3,
12
+    "name": "Chocolate"
13
+  },
14
+  {
15
+    "id": 4,
16
+    "name": "Snacks"
17
+  },
18
+  {
19
+    "id": 5,
20
+    "name": "Ice Cream"
21
+  }
22
+]

+ 12
- 20
src/components/CardMenu.vue 查看文件

@@ -1,30 +1,22 @@
1 1
 <template>
2 2
   <div class="card-menu rounded">
3 3
     <div class="ratio ratio-1x1 rounded overflow-hidden">
4
-      <img :src="`/img/${data.image}`" :alt="data.name" class="object-fit-cover">
4
+      <img :src="`/img/${props.data.image}`" :alt="props.data.name" class="object-fit-cover">
5 5
     </div>
6
-    <div class="mt-2 mb-1 fw-semibold">{{ data.name }}</div>
7
-    <div class="menu-desc lh-sm mb-1">{{ data.description }}</div>
8
-    <p class="fw-medium mb-2"><sup class="text-dark-purple">Rp</sup>{{ rupiah(data.price) }}</p>
9
-    <button class="btn btn-primary w-100 text-white btn-sm">Add to Cart</button>
6
+    <div class="mt-2 mb-1 fw-semibold">{{ props.data.name }}</div>
7
+    <div class="menu-desc lh-sm mb-1">{{ props.data.description }}</div>
8
+    <p class="fw-medium mb-2"><sup class="text-dark-purple">Rp</sup>{{ $rupiah(props.data.price) }}</p>
9
+    <button class="btn btn-primary w-100 text-white btn-sm" @click="cart.addItem(props.data)">Add to Cart</button>
10 10
   </div>
11 11
 </template>
12 12
 
13
-<script>
14
-export default {
15
-  name: 'CardMenu',
16
-  props: {
17
-    data: {
18
-      type: Object,
19
-      required: true
20
-    }
21
-  },
22
-  methods: {
23
-    rupiah(number) {
24
-      return Number(number).toLocaleString('id-ID')
25
-    }
26
-  }
27
-}
13
+<script setup>
14
+import { useCartStore } from '../stores/cart';
15
+const cart = useCartStore()
16
+
17
+const props = defineProps({
18
+  data: Object
19
+})
28 20
 </script>
29 21
 
30 22
 <style lang="scss" scoped>

+ 17
- 6
src/components/CartItem.vue 查看文件

@@ -2,19 +2,19 @@
2 2
   <div class="order-list-item position-relative mb-3">
3 3
     <div class="d-flex align-items-center gap-2">
4 4
       <div class="ratio ratio-1x1 rounded overflow-hidden">
5
-        <img :src="'/img/iced-choco.jpg'" class="object-fit-cover" alt="Chocolate">
5
+        <img :src="`/img/${dataMenu.image}`" class="object-fit-cover" alt="Chocolate">
6 6
       </div>
7 7
       <div>
8
-        <p class="mb-1">Expresso</p>
9
-        <p class="mb-0"><small>Rp21.000</small></p>
8
+        <p class="mb-1">{{ dataMenu.name }}</p>
9
+        <p class="mb-0"><small>Rp{{ $rupiah(dataMenu.price) }}</small></p>
10 10
       </div>
11 11
     </div>
12 12
     <div class="position-absolute bottom-0 end-0 d-flex align-items-center gap-2">
13
-      <Icon icon="iconoir:minus" width="18" class="icon-minus" />
13
+      <Icon icon="iconoir:minus" width="18" class="icon-minus" @click="decreaseQty(dataMenu.id)" />
14 14
       <span class="qty text-center">
15
-        <input type="number" class="form-control" value="2" min="0">
15
+        <input type="number" class="form-control" :value="dataMenu.qty" min="0">
16 16
       </span>
17
-      <Icon icon="iconoir:plus" width="18" class="icon-plus" />
17
+      <Icon icon="iconoir:plus" width="18" class="icon-plus" @click="increaseQty(dataMenu.id)" />
18 18
     </div>
19 19
   </div>
20 20
 </template>
@@ -26,10 +26,21 @@ export default {
26 26
   name: 'CartItem',
27 27
   components: {
28 28
     Icon
29
+  },
30
+  props: {
31
+    dataMenu: {
32
+      type: Object,
33
+      required: true
34
+    }
29 35
   }
30 36
 }
31 37
 </script>
32 38
 
39
+<script setup>
40
+import { useCartStore } from '../stores/cart'
41
+const { increaseQty, decreaseQty } = useCartStore()
42
+</script>
43
+
33 44
 <style lang="scss" scoped>
34 45
 .ratio {
35 46
   max-width: 50px;

+ 11
- 20
src/components/MenuContainer.vue 查看文件

@@ -1,7 +1,9 @@
1 1
 <template>
2 2
   <div class="pb-4">
3 3
     <h5 class="mb-3">Menu</h5>
4
-    <div class="row g-4 row-cols-lg-3">
4
+    <p v-if="loading">Loading menu ...</p>
5
+    <p v-if="error">{{ error.message }}</p>
6
+    <div v-if="menus" class="row g-4 row-cols-lg-3">
5 7
       <div v-for="menu in menus" :key="menu.id" class="col">
6 8
         <CardMenu :data="menu" />
7 9
       </div>
@@ -9,26 +11,15 @@
9 11
   </div>
10 12
 </template>
11 13
 
12
-<script>
13
-import CardMenu from './CardMenu.vue'; './CardMenu.vue'
14
+<script setup>
15
+import CardMenu from './CardMenu.vue'
16
+import { storeToRefs } from 'pinia'
17
+import { useMenuStore } from '../stores/menus'
14 18
 
15
-export default {
16
-  name: 'MenuContainer',
17
-  components: {
18
-    CardMenu
19
-  },
20
-  data() {
21
-    return {
22
-      menus: []
23
-    }
24
-  },
25
-  async mounted() {
26
-    await fetch('menu.json').then(resp => resp.json()).then(data => {
27
-      console.log(data)
28
-      this.menus = data
29
-    })
30
-  }
31
-}
19
+const { menus, loading, error } = storeToRefs(useMenuStore())
20
+const { fetchMenus } = useMenuStore()
21
+
22
+fetchMenus()
32 23
 </script>
33 24
 
34 25
 <style lang="scss" scoped></style>

+ 31
- 17
src/components/OrderList.vue 查看文件

@@ -21,34 +21,33 @@
21 21
     </div>
22 22
     <div class="row mt-4">
23 23
       <div class="col">
24
-        <div class="total-items">Total items:</div>
24
+        <div class="total-items">Total items: {{ getTotalItems }}</div>
25 25
       </div>
26 26
       <div class="col text-end">
27
-        <button class="btn btn-sm btn-outline-red btn-remove">Remove all</button>
27
+        <button v-if="getTotalItems > 0" class="btn btn-sm btn-outline-red btn-remove" @click="cartStore.$reset">Remove
28
+          all</button>
28 29
       </div>
29 30
     </div>
30
-    <div class="order-list mt-3">
31
-      <CartItem />
32
-      <CartItem />
33
-      <CartItem />
34
-      <CartItem />
31
+    <p v-if="getTotalItems === 0" class="text-center mt-3 mb-5">The order list is empty</p>
32
+    <div v-else class="order-list mt-3">
33
+      <CartItem v-for="item in carts" :key="item.id" :dataMenu="item" />
35 34
     </div>
36
-    <div class="total-payment px-3 py-2 mt-3">
35
+    <div v-if="getTotalItems > 0" class="total-payment px-3 py-2 mt-3">
37 36
       <div class="row">
38 37
         <div class="col">Subtotal</div>
39
-        <div class="col text-end fw-medium">Rp68.000</div>
38
+        <div class="col text-end fw-medium">Rp{{ $rupiah(getTotalPrice) }}</div>
40 39
       </div>
41 40
       <div class="row">
42
-        <div class="col">Tax</div>
43
-        <div class="col text-end fw-medium">Rp3.400</div>
41
+        <div class="col">Tax 5%</div>
42
+        <div class="col text-end fw-medium">Rp{{ $rupiah(getTax) || 0 }}</div>
44 43
       </div>
45 44
       <hr>
46 45
       <div class="row text-dark-purple fw-bold">
47 46
         <div class="col">Total</div>
48
-        <div class="col text-end">Rp71.400</div>
47
+        <div class="col text-end">Rp{{ $rupiah(totalPayment) }}</div>
49 48
       </div>
50 49
     </div>
51
-    <div class="payment-methods row">
50
+    <div v-if="getTotalItems > 0" class="payment-methods row">
52 51
       <div v-for="payment in paymentOption" :key="payment.id" class="col"
53 52
         :class="{ selected: order.paymentMethod === payment.name }" @click="order.paymentMethod = payment.name">
54 53
         <div class="d-flex gap-2 align-items-center justify-content-center">
@@ -57,12 +56,15 @@
57 56
         </div>
58 57
       </div>
59 58
     </div>
60
-    <button class="btn btn-checkout text-white w-100 fw-bold">Process order</button>
59
+    <button class="btn btn-checkout text-white w-100 fw-bold" :disabled="getTotalItems < 1">Process order</button>
61 60
   </div>
62 61
 </template>
63 62
 
64 63
 <script>
65
-import { Icon } from '@iconify/vue';
64
+import { computed } from 'vue'
65
+import { Icon } from '@iconify/vue'
66
+import { storeToRefs } from 'pinia'
67
+import { useCartStore } from '../stores/cart'
66 68
 import CartItem from '../components/CartItem.vue'
67 69
 
68 70
 export default {
@@ -73,7 +75,6 @@ export default {
73 75
   },
74 76
   data() {
75 77
     return {
76
-      cart: [],
77 78
       paymentOption: [
78 79
         {
79 80
           id: 1,
@@ -102,6 +103,18 @@ export default {
102 103
 }
103 104
 </script>
104 105
 
106
+<script setup>
107
+const { carts, getTotalItems, getTotalPrice } = storeToRefs(useCartStore())
108
+const cartStore = useCartStore()
109
+const getTax = computed(() => {
110
+  // console.log(getTotalPrice)
111
+  return (getTotalPrice.value * 0.05)
112
+})
113
+const totalPayment = computed(() => {
114
+  return getTotalPrice.value + getTax.value
115
+})
116
+</script>
117
+
105 118
 <style lang="scss" scoped>
106 119
 .order-details {
107 120
   border-radius: 6px;
@@ -119,6 +132,7 @@ export default {
119 132
   justify-content: center;
120 133
   align-items: center;
121 134
   font-weight: 500;
135
+  min-height: 28px;
122 136
 }
123 137
 
124 138
 .btn-remove {
@@ -131,7 +145,7 @@ export default {
131 145
 }
132 146
 
133 147
 .order-list {
134
-  max-height: 156px;
148
+  max-height: 158px;
135 149
   overflow-y: auto;
136 150
 }
137 151
 

+ 6
- 0
src/main.js 查看文件

@@ -1,11 +1,17 @@
1 1
 import './assets/main.css'
2 2
 
3 3
 import { createApp } from 'vue'
4
+import { createPinia } from 'pinia'
5
+
4 6
 import App from './App.vue'
5 7
 import router from './router'
6 8
 
7 9
 const app = createApp(App)
8 10
 
11
+app.config.globalProperties.$rupiah = (number) => {
12
+  return Number(number).toLocaleString('id-ID')
13
+}
14
+app.use(createPinia())
9 15
 app.use(router)
10 16
 
11 17
 app.mount('#app')

+ 59
- 0
src/stores/cart.js 查看文件

@@ -0,0 +1,59 @@
1
+import { defineStore } from "pinia"
2
+
3
+export const useCartStore = defineStore('cart', {
4
+  state: () => ({
5
+    carts: [],
6
+  }),
7
+  getters: {
8
+    getTotalItems: (state) => {
9
+      // console.log('muncul gak nih')
10
+      if (state.carts.length) {
11
+        return state.carts?.reduce((total, item) => {
12
+          // console.log(total)
13
+          // console.log(item.qty)
14
+          return total + item.qty
15
+        }, 0)
16
+      }
17
+      return 0
18
+    },
19
+    getTotalPrice: (state) => {
20
+      if (state.carts.length) {
21
+        return state.carts?.reduce((total, item) => {
22
+          // console.log(item.price)
23
+          return total + item.price * item.qty
24
+        }, 0)
25
+      }
26
+      return 0
27
+    }
28
+  },
29
+  actions: {
30
+    addItem(product) {
31
+      // console.log(product)
32
+      const existingItem = this.carts.find((item) => item.id === product.id)
33
+
34
+      if (existingItem) {
35
+        existingItem.qty += 1
36
+      } else {
37
+        this.carts.push({ ...product, qty: 1 })
38
+      }
39
+      // console.log(this.carts)
40
+    },
41
+    increaseQty(id) {
42
+      const itemIndex = this.carts.findIndex(item => item.id === id)
43
+      this.carts[itemIndex].qty++
44
+    },
45
+    decreaseQty(id) {
46
+      const itemIndex = this.carts.findIndex(item => item.id === id)
47
+
48
+      if (this.carts[itemIndex].qty <= 1) {
49
+        this.removeItem(id)
50
+      } else {
51
+        this.carts[itemIndex].qty--
52
+      }
53
+    },
54
+    removeItem(id) {
55
+      const itemIndex = this.carts.findIndex(item => item.id === id)
56
+      this.carts.splice(itemIndex, 1)
57
+    }
58
+  }
59
+})

+ 27
- 0
src/stores/menus.js 查看文件

@@ -0,0 +1,27 @@
1
+import { defineStore } from "pinia"
2
+
3
+export const useMenuStore = defineStore('menus', {
4
+  state: () => ({
5
+    menus: [],
6
+    loading: false,
7
+    error: null
8
+  }),
9
+  getters: {
10
+    getMenuPerCategory: (state) => {
11
+      return (categoryName) => state.menus.filter((item) => item.category === categoryName)
12
+    }
13
+  },
14
+  actions: {
15
+    async fetchMenus() {
16
+      this.menus = []
17
+      this.loading = true
18
+      try {
19
+        this.menus = await fetch('menu.json').then((response) => response.json())
20
+      } catch (error) {
21
+        this.error = error
22
+      } finally {
23
+        this.loading = false
24
+      }
25
+    }
26
+  }
27
+})

Loading…
取消
儲存