@mixd-id/web-scaffold 0.1.240411024 → 0.1.240411025

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@mixd-id/web-scaffold",
3
3
  "private": false,
4
- "version": "0.1.240411024",
4
+ "version": "0.1.240411025",
5
5
  "scripts": {
6
6
  "dev": "vite serve",
7
7
  "build": "vite build",
@@ -1,21 +1,33 @@
1
1
  <template>
2
2
  <div :class="$style.el">
3
3
 
4
- <div v-if="readyState === 2" class="flex justify-center items-center min-h-[80vh]">
4
+ <div v-if="order === 'loading'" class="flex justify-center items-center min-h-[80vh]">
5
5
  Loading...
6
6
  </div>
7
7
 
8
- <div v-else-if="!order" class="flex flex-col gap-4 justify-center items-center min-h-[80vh] p-8">
9
- <div class="text-center">
10
- <h1>Cart is Empty</h1>
11
- <p class="max-w-[480px]">Your order is empty, if you're interested in our products, browse products from our catalog by clicking button below.</p>
12
- </div>
13
- <router-link to="/" class="text-primary">Browse Catalog</router-link>
8
+ <div v-else-if="order instanceof Error"
9
+ class="flex flex-col gap-4 justify-center items-center min-h-[80vh] p-8">
10
+ <slot name="error" :error="order">
11
+ <component v-if="items"
12
+ v-for="component in items"
13
+ :is="component"
14
+ :key="component"
15
+ :="component" />
16
+ <div v-else class="flex flex-col gap-3 items-center justify-center">
17
+ <h1>Error</h1>
18
+ <p class="max-w-[480px]">{{ order.message }}</p>
19
+ </div>
20
+ </slot>
14
21
  </div>
15
22
 
16
23
  <div v-else class="flex flex-col md:flex-row md:gap-8 min-h-[100vh]">
17
24
 
18
25
  <div class="flex-1 flex flex-col gap-5">
26
+
27
+ <div class="p-5">
28
+ <h3>Checkout</h3>
29
+ </div>
30
+
19
31
  <div v-if="order.customerId">
20
32
  <div class="px-5">
21
33
  <small class="text-text-400">Customer</small>
@@ -28,10 +40,10 @@
28
40
  </div>
29
41
 
30
42
  <div v-else>
31
- <div class="px-5">
32
- <small class="text-text-400">Customer</small>
43
+ <div class="px-5 flex flex-row gap-2">
44
+ <small class="flex-1 text-text-400">Customer</small>
33
45
  </div>
34
- <div class="bg-base-300 flex flex-col gap-3 p-5 py-3">
46
+ <div class="mt-1 bg-base-300 flex flex-col gap-3 p-5 py-3">
35
47
  <div>
36
48
  <small>Name</small>
37
49
  <Textbox v-model="order.customerName" />
@@ -47,17 +59,36 @@
47
59
  </div>
48
60
  </div>
49
61
 
62
+ <div>
63
+ <div class="px-5">
64
+ <small class="text-text-400">Products</small>
65
+ </div>
66
+
67
+ <div class="mt-1 p-5 bg-base-300 flex flex-col gap-3">
68
+ <div v-for="item in order.items" class="flex flex-row gap-3">
69
+ <Image :src="item.imageSrc" class="w-[60px] aspect-square rounded-lg bg-base-50" />
70
+ <div class="flex-1 flex flex-col gap1">
71
+ <small>{{ item.code }}</small>
72
+ <strong>{{ item.name }}</strong>
73
+ <label>{{ item.qty }}x {{ item.price.toLocaleString() }}</label>
74
+ </div>
75
+ </div>
76
+ </div>
77
+ </div>
78
+
50
79
  <div>
51
80
  <div class="px-5 flex flex-row gap-2">
52
- <small class="text-text-400 flex-1">Delivery</small>
81
+ <small class="text-text-400 flex-1">Delivery Address</small>
53
82
  <button type="button" class="text-primary text-sm"
54
- @click="$refs.deliveryAddress.open()">
83
+ @click="$router.push({ query:$route.query, hash:'#select-delivery' })">
55
84
  Choose Address
56
85
  </button>
57
86
  </div>
58
- <div class="bg-base-300 flex flex-col p-5 py-3">
87
+ <div class="bg-base-300 flex flex-col gap-5 p-5 py-3 mt-1">
59
88
 
60
- <div v-if="order.deliveryAddressId">
89
+ <div class="flex flex-col gap-1">
90
+ <small>{{ order.deliveryType }}</small>
91
+ <strong>{{ order.deliveryContactName }}</strong>
61
92
  <p>{{ order.deliveryAddress }}</p>
62
93
  <p>
63
94
  {{ order.deliveryProvinceName }}
@@ -73,6 +104,30 @@
73
104
  </p>
74
105
  </div>
75
106
 
107
+ <div class="h-[1px] bg-text-50"></div>
108
+
109
+ <div class="grid grid-cols-2 gap-3">
110
+ <div>
111
+ <small>Courier</small>
112
+ <Dropdown v-model.number="order.deliveryCourierId">
113
+ <option disabled selected>Select Courier</option>
114
+ <option v-for="courier in couriers" :value="courier.id">
115
+ {{ courier.name }}
116
+ </option>
117
+ </Dropdown>
118
+ </div>
119
+
120
+ <div>
121
+ <small>Package</small>
122
+ <Dropdown v-model.number="order.deliveryPackageId">
123
+ <option disabled selected>Select Package</option>
124
+ <option v-for="obj in courierPackages" :value="obj.id">
125
+ {{ obj.name }}
126
+ </option>
127
+ </Dropdown>
128
+ </div>
129
+ </div>
130
+
76
131
  </div>
77
132
  </div>
78
133
 
@@ -80,18 +135,42 @@
80
135
  <div class="px-5 flex flex-row gap-2">
81
136
  <small class="text-text-400">Payment</small>
82
137
  </div>
83
- <div class="bg-base-300 p-5 py-3 grid grid-cols-2 gap-3">
138
+ <div class="bg-base-300 p-5 py-3 grid md:grid-cols-2 gap-3">
84
139
 
85
140
  <Radio v-for="paymentType of paymentTypes"
86
141
  :class="`${$style.paymentType}${paymentType.id === order.paymentTypeId ? ' ' + $style.paymentTypeSelected : ''}`"
87
142
  v-model.number="order.paymentTypeId"
88
- :value="paymentType.id"
89
- :custom="true">
143
+ :value="paymentType.id">
90
144
  {{ paymentType.name }}
91
145
  </Radio>
92
146
 
93
147
  </div>
94
148
  </div>
149
+
150
+ <div>
151
+ <div class="px-5 flex flex-row gap-2">
152
+ <small class="text-text-400">Summary</small>
153
+ </div>
154
+ <div class="bg-base-300 mt-1 p-5 flex flex-col gap-3">
155
+
156
+ <div class="flex flex-row gap-2">
157
+ <label class="flex-1">Total Products</label>
158
+ <strong>{{ (order.subTotal ?? 0).toLocaleString() }}</strong>
159
+ </div>
160
+
161
+ <div class="flex flex-row gap-2">
162
+ <label class="flex-1">Discount</label>
163
+ <strong>{{ (order.discountAmount ?? 0).toLocaleString() }}</strong>
164
+ </div>
165
+
166
+ <div class="flex flex-row gap-2">
167
+ <label class="flex-1">Delivery Charge</label>
168
+ <strong>{{ (order.deliveryCharge ?? 0).toLocaleString() }}</strong>
169
+ </div>
170
+
171
+ </div>
172
+ </div>
173
+
95
174
  </div>
96
175
 
97
176
  <div class="flex md:hidden p-5 py-3 bg-base-300 flex-row sticky bottom-0 border-t-[1px] border-text-50">
@@ -101,7 +180,7 @@
101
180
  </div>
102
181
  <div v-else class="flex-1"></div>
103
182
  <Button ref="paymentBtn"
104
- class="px-3"
183
+ class="px-8 font-bold"
105
184
  :state="canPayment ? 1 : -1"
106
185
  @click="payment">
107
186
  Payment
@@ -121,37 +200,25 @@
121
200
 
122
201
  </div>
123
202
 
124
- <Modal ref="deliveryAddress" width="480" height="600">
125
- <template v-slot:head>
126
- <div class="relative p-5">
127
- <h3>Select Address</h3>
128
- <div class="absolute top-0 right-0 p-2">
129
- <button type="button" class="p-2" @click="$refs.deliveryAddress.close()">
130
- <svg width="24" height="24" viewBox="0 0 24 24" class="fill-text-300 hover:fill-red-500" xmlns="http://www.w3.org/2000/svg">
131
- <path d="M6.53034 5.46965C6.23745 5.17676 5.76257 5.17676 5.46968 5.46965C5.17679 5.76255 5.17679 6.23742 5.46968 6.53031L10.9393 12L5.46967 17.4697C5.17678 17.7626 5.17678 18.2374 5.46967 18.5303C5.76256 18.8232 6.23744 18.8232 6.53033 18.5303L12 13.0606L17.4697 18.5303C17.7626 18.8232 18.2375 18.8232 18.5303 18.5303C18.8232 18.2374 18.8232 17.7626 18.5303 17.4697L13.0607 12L18.5303 6.53032C18.8232 6.23743 18.8232 5.76256 18.5303 5.46966C18.2374 5.17677 17.7626 5.17677 17.4697 5.46966L12 10.9393L6.53034 5.46965Z"/>
132
- </svg>
133
- </button>
134
- </div>
135
- </div>
136
- </template>
137
-
203
+ <Modal hash="#select-delivery"
204
+ width="400"
205
+ height="640"
206
+ :dismissable="true">
138
207
  <template #default="{ context }">
139
- <div class="flex-1 p-5 flex flex-col">
140
-
141
- <div v-for="deliveryAddress in deliveryAddresses">
142
- {{ deliveryAddress }}
208
+ <div class="flex-1 flex flex-col">
209
+ <div class="absolute top-0 right-0">
210
+ <button type="button"
211
+ class="p-5"
212
+ @click="$router.replace({ query:$route.query, hash:'' })">
213
+ <svg width="19" height="19" class="fill-text" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 512"><!--! Font Awesome Pro 6.0.0-alpha3 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) --><path d="M310.6 361.4c12.5 12.5 12.5 32.75 0 45.25C304.4 412.9 296.2 416 288 416s-16.38-3.125-22.62-9.375L160 301.3L54.63 406.6C48.38 412.9 40.19 416 32 416S15.63 412.9 9.375 406.6c-12.5-12.5-12.5-32.75 0-45.25l105.4-105.4L9.375 150.6c-12.5-12.5-12.5-32.75 0-45.25s32.75-12.5 45.25 0L160 210.8l105.4-105.4c12.5-12.5 32.75-12.5 45.25 0s12.5 32.75 0 45.25l-105.4 105.4L310.6 361.4z"/></svg>
214
+ </button>
143
215
  </div>
144
216
 
145
- <button type="button" class="p-3 text-center">
146
- New Address
147
- </button>
148
-
149
- </div>
150
- </template>
151
-
152
- <template v-slot:foot>
153
- <div class="p-5">
154
-
217
+ <CheckoutDelivery class="flex-1"
218
+ :api-url="apiUrl"
219
+ ref="checkoutDelivery"
220
+ :items="deliveryAddresses"
221
+ @apply="setDelivery" />
155
222
  </div>
156
223
  </template>
157
224
  </Modal>
@@ -162,28 +229,45 @@
162
229
  <script>
163
230
 
164
231
  import axios from "axios";
232
+ import CheckoutDelivery from "./CheckoutDelivery.vue";
165
233
 
166
234
  export default {
167
235
 
236
+ components: {CheckoutDelivery},
237
+
238
+ inject: [ 'toast' ],
239
+
168
240
  computed: {
169
241
 
170
242
  canPayment(){
171
243
  return this.apiUrl &&
172
244
  this.order
245
+ },
246
+
247
+ courierPackages(){
248
+ const courier = (this.couriers ?? [])
249
+ .find(_ => _.id === this.order.deliveryCourierId)
250
+
251
+ return courier?.packages
173
252
  }
174
253
 
175
254
  },
176
255
 
177
256
  props: {
178
257
 
179
- apiUrl: String
258
+ apiUrl: {
259
+ type: String,
260
+ required: true
261
+ },
262
+
263
+ items: Array
180
264
 
181
265
  },
182
266
 
183
267
  methods: {
184
268
 
185
269
  load(uid){
186
- this.readyState = 2
270
+ this.order = 'loading'
187
271
  axios.get(import.meta.env.VITE_API_HOST + this.apiUrl, {
188
272
  params: {
189
273
  uid
@@ -193,16 +277,39 @@ export default {
193
277
  this.order = res.data.order
194
278
  this.deliveryAddresses = res.data.deliveryAddresses
195
279
  this.paymentTypes = res.data.paymentTypes
196
- this.readyState = 1
197
- })
198
- .catch(err => {
199
- this.readyState = -1
200
- this.error = err
201
280
  })
281
+ .catch(err => this.order = err)
282
+ },
283
+
284
+ loadCouriers(){
285
+ if(!this.apiUrl)
286
+ return console.warn('Unable to load couriers, apiUrl not set')
287
+
288
+ axios.get(import.meta.env.VITE_API_HOST + this.apiUrl, {
289
+ params: {
290
+ _action: 'load-couriers'
291
+ }
292
+ })
293
+ .then(res => this.couriers = res.data)
294
+ .catch(err => this.toast(err))
202
295
  },
203
296
 
204
297
  payment(){
205
298
 
299
+ },
300
+
301
+ update(){
302
+ axios.post(import.meta.env.VITE_API_HOST + this.apiUrl, {
303
+ _action: 'update',
304
+ order: this.order
305
+ })
306
+ .then(res => Object.assign(this.order, res.data))
307
+ .catch(err => this.toast(err))
308
+ },
309
+
310
+ setDelivery(address){
311
+ Object.assign(this.order, address)
312
+ this.$router.replace({ query:this.$route.query, hash:'' })
206
313
  }
207
314
 
208
315
  },
@@ -212,14 +319,14 @@ export default {
212
319
  order: null,
213
320
  paymentTypes: null,
214
321
  deliveryAddresses: null,
215
- readyState: 1,
216
- error: null
322
+
323
+ couriers: null
217
324
  }
218
325
  },
219
326
 
220
327
  mounted() {
221
328
  if(this.$editMode.value > 0){
222
- this.load('dummy')
329
+ this.order = {}
223
330
  }
224
331
  },
225
332
 
@@ -232,6 +339,15 @@ export default {
232
339
  this.load(uid)
233
340
  }
234
341
  }
342
+ },
343
+
344
+ 'order.deliveryAddress': {
345
+ immediate: true,
346
+ handler(deliveryAddress){
347
+ if(deliveryAddress){
348
+ this.loadCouriers()
349
+ }
350
+ }
235
351
  }
236
352
 
237
353
  }
@@ -247,7 +363,7 @@ export default {
247
363
  }
248
364
 
249
365
  .paymentType{
250
- @apply border-[1px] border-text-50 rounded-lg;
366
+ @apply rounded-lg;
251
367
  @apply p-2 text-sm;
252
368
  }
253
369
  .paymentTypeSelected{
@@ -0,0 +1,267 @@
1
+ <template>
2
+ <div class="flex-1 flex flex-col">
3
+
4
+ <div class="p-5">
5
+ <h3>Select Delivery</h3>
6
+ </div>
7
+
8
+ <div v-if="!newItem" class="flex flex-col divide-y divide-text-50">
9
+ <div v-for="item in items"
10
+ class="p-5 py-3 flex flex-col cursor-pointer"
11
+ @click="apply(item)">
12
+ <small>{{ item.deliveryType }}</small>
13
+ <strong>{{ item.deliveryContactName }}</strong>
14
+ <p>{{ item.deliveryAddress }}</p>
15
+ </div>
16
+
17
+ <button type="button" class="p-6 text-primary" @click="addNew">
18
+ New Address
19
+ </button>
20
+ </div>
21
+
22
+ <div v-else class="flex-1 flex flex-col gap-5">
23
+
24
+ <div class="flex-1 overflow-y-auto flex flex-col gap-5 p-5">
25
+ <div>
26
+ <small>Type</small>
27
+ <Dropdown v-model="newItem.deliveryType">
28
+ <option>Home</option>
29
+ <option>Office</option>
30
+ </Dropdown>
31
+ </div>
32
+
33
+ <div>
34
+ <small>Contact Name</small>
35
+ <Textbox v-model="newItem.deliveryContactName" />
36
+ </div>
37
+
38
+ <div>
39
+ <small>Address</small>
40
+ <Textarea v-model="newItem.deliveryAddress" rows="3" />
41
+ </div>
42
+
43
+ <div>
44
+ <small>Province</small>
45
+ <Dropdown v-model="newItem.deliveryProvinceName">
46
+ <option disabled selected>Choose Province</option>
47
+ <option v-for="province in provinces" :value="province.name">
48
+ {{ province.name }}
49
+ </option>
50
+ </Dropdown>
51
+ </div>
52
+
53
+ <div>
54
+ <small>City</small>
55
+ <Dropdown v-model="newItem.deliveryCityName">
56
+ <option disabled selected>Choose City</option>
57
+ <option v-for="city in cities" :value="city.name">
58
+ {{ city.name }}
59
+ </option>
60
+ </Dropdown>
61
+ </div>
62
+
63
+ <div>
64
+ <small>District</small>
65
+ <Dropdown v-model="newItem.deliveryDistrictName">
66
+ <option disabled selected>Choose District</option>
67
+ <option v-for="district in districts" :value="district.name">
68
+ {{ district.name }}
69
+ </option>
70
+ </Dropdown>
71
+ </div>
72
+
73
+ <div>
74
+ <small>Sub District</small>
75
+ <Dropdown v-model="newItem.deliverySubDistrictName"
76
+ @change="setPostalCode(newItem.deliverySubDistrictName)">
77
+ <option disabled selected>Choose Sub District</option>
78
+ <option v-for="subDistrict in subDistricts" :value="subDistrict.name">
79
+ {{ subDistrict.name }}
80
+ </option>
81
+ </Dropdown>
82
+ </div>
83
+
84
+ <div>
85
+ <small>Postal Code</small>
86
+ <Textbox v-model="newItem.deliveryPostalCode" />
87
+ </div>
88
+ </div>
89
+
90
+ <div class="p-5">
91
+ <Button class="w-full"
92
+ :state="canSave ? 1 : -1"
93
+ @click="apply(newItem)">Set Address</Button>
94
+ </div>
95
+
96
+ </div>
97
+
98
+ </div>
99
+ </template>
100
+
101
+ <script>
102
+
103
+ import axios from "axios";
104
+
105
+ export default{
106
+
107
+ emits: [ 'apply' ],
108
+
109
+ computed: {
110
+
111
+ canSave(){
112
+ return this.newItem &&
113
+ this.newItem.deliveryType &&
114
+ this.newItem.deliveryContactName &&
115
+ this.newItem.deliveryAddress &&
116
+ this.newItem.deliveryProvinceName &&
117
+ this.newItem.deliveryCityName &&
118
+ this.newItem.deliveryDistrictName &&
119
+ this.newItem.deliverySubDistrictName &&
120
+ this.newItem.deliveryPostalCode
121
+ }
122
+
123
+ },
124
+
125
+ props: {
126
+
127
+ apiUrl: {
128
+ type: String,
129
+ required: true
130
+ },
131
+
132
+ items: Array
133
+
134
+ },
135
+
136
+ data(){
137
+ return {
138
+ newItem: null,
139
+
140
+ provinces: null,
141
+ cities: null,
142
+ districts: null,
143
+ subDistricts: null
144
+ }
145
+ },
146
+
147
+ methods: {
148
+
149
+ addNew(){
150
+ this.newItem = {}
151
+ this.loadProvinces()
152
+ },
153
+
154
+ apply(item){
155
+ this.$emit('apply', JSON.parse(JSON.stringify(item)))
156
+ },
157
+
158
+ loadProvinces(){
159
+ axios.get(import.meta.env.VITE_API_HOST + this.apiUrl, {
160
+ params: {
161
+ _action: 'load-provinces'
162
+ }
163
+ })
164
+ .then(res => {
165
+ this.provinces = res.data
166
+ })
167
+ .catch(err => {
168
+ console.error(err)
169
+ })
170
+ },
171
+
172
+ loadCities(){
173
+ axios.get(import.meta.env.VITE_API_HOST + this.apiUrl, {
174
+ params: {
175
+ _action: 'load-cities',
176
+ province: this.newItem.deliveryProvinceName
177
+ }
178
+ })
179
+ .then(res => {
180
+ this.cities = res.data
181
+ })
182
+ .catch(err => {
183
+ console.error(err)
184
+ })
185
+ },
186
+
187
+ loadDistricts(){
188
+ axios.get(import.meta.env.VITE_API_HOST + this.apiUrl, {
189
+ params: {
190
+ _action: 'load-districts',
191
+ province: this.newItem.deliveryProvinceName,
192
+ city: this.newItem.deliveryCityName
193
+ }
194
+ })
195
+ .then(res => {
196
+ this.districts = res.data
197
+ })
198
+ .catch(err => {
199
+ console.error(err)
200
+ })
201
+ },
202
+
203
+ loadSubDistricts(){
204
+ axios.get(import.meta.env.VITE_API_HOST + this.apiUrl, {
205
+ params: {
206
+ _action: 'load-sub-districts',
207
+ province: this.newItem.deliveryProvinceName,
208
+ city: this.newItem.deliveryCityName,
209
+ district: this.newItem.deliveryDistrictName
210
+ }
211
+ })
212
+ .then(res => {
213
+ this.subDistricts = res.data
214
+ })
215
+ .catch(err => {
216
+ console.error(err)
217
+ })
218
+ },
219
+
220
+ setPostalCode(subDistrictName){
221
+ const subDistrict = (this.subDistricts ?? []).find(item => item.name === subDistrictName)
222
+ this.newItem.deliveryPostalCode = subDistrict?.postalCode ?? ''
223
+ },
224
+
225
+ },
226
+
227
+ watch: {
228
+
229
+ 'newItem.deliveryProvinceName': {
230
+ immediate: true,
231
+ handler(to){
232
+ if(to)
233
+ this.loadCities()
234
+ }
235
+ },
236
+
237
+ 'newItem.deliveryCityName': {
238
+ immediate: true,
239
+ handler(to){
240
+ if(to)
241
+ this.loadDistricts()
242
+ }
243
+ },
244
+
245
+ 'newItem.deliveryDistrictName': {
246
+ immediate: true,
247
+ handler(to){
248
+ if(to)
249
+ this.loadSubDistricts()
250
+ }
251
+ }
252
+
253
+ }
254
+
255
+
256
+
257
+ }
258
+
259
+ </script>
260
+
261
+ <style module>
262
+
263
+ .comp{
264
+
265
+ }
266
+
267
+ </style>
@@ -404,6 +404,7 @@ html[data-theme='dark'] .overlay{
404
404
  @media screen and (max-width: 640px){
405
405
 
406
406
  .modal {
407
+ width: 100% !important;
407
408
  max-height: 90vh;
408
409
  }
409
410
 
@@ -429,11 +430,5 @@ html[data-theme='dark'] .overlay{
429
430
 
430
431
  }
431
432
 
432
- @media screen and (min-width: 640px) {
433
-
434
- .modal {
435
- }
436
-
437
- }
438
433
 
439
434
  </style>