瀏覽代碼

see also #383

ali 2 年之前
父節點
當前提交
7d797cf0eb

+ 4 - 0
src/api/APIGateway.js

@@ -10,6 +10,7 @@ import MediaAPI from 'src/api/models/Media.js'
 import NoticeAPI from 'src/api/models/Notice.js'
 import TicketAPI from 'src/api/models/Ticket.js'
 import WalletAPI from 'src/api/models/Wallet.js'
+import BasketAPI from 'src/api/models/Basket.js'
 import TestSetAPI from 'src/api/models/TestSet.js'
 import InvoiceAPI from 'src/api/models/Invoice.js'
 import PaymentAPI from 'src/api/models/Payment.js'
@@ -23,6 +24,7 @@ import QuestionApi from 'src/api/models/Question.js'
 import ClassroomAPI from 'src/api/models/Classroom.js'
 import AnswerBookApi from 'src/api/models/AnswerBook.js'
 import InstalmentAPI from 'src/api/models/Instalment.js'
+import BasketItemAPI from 'src/api/models/BasketItem.js'
 import AnswerSheetApi from 'src/api/models/AnswerSheet.js'
 import TransactionAPI from 'src/api/models/Transaction.js'
 import PageSettingAPI from 'src/api/models/PageSetting.js'
@@ -62,6 +64,7 @@ export const APIGateway = {
   notice: new NoticeAPI(),
   ticket: new TicketAPI(),
   wallet: new WalletAPI(),
+  basket: new BasketAPI(),
   testSet: new TestSetAPI(),
   project: new ProjectAPI(),
   package: new PackageAPI(),
@@ -75,6 +78,7 @@ export const APIGateway = {
   classroom: new ClassroomAPI(),
   answerBook: new AnswerBookApi(),
   instalment: new InstalmentAPI(),
+  basketItem: new BasketItemAPI(),
   answerSheet: new AnswerSheetApi(),
   transaction: new TransactionAPI(),
   pageSetting: new PageSettingAPI(),

+ 154 - 0
src/api/models/Basket.js

@@ -0,0 +1,154 @@
+import { appApi } from 'src/boot/axios.js'
+import APIRepository from '../classes/APIRepository.js'
+import { Basket, BasketList } from 'src/models/Basket.js'
+
+export default class BasketAPI extends APIRepository {
+  constructor() {
+    super('package', appApi)
+    this.APIAdresses = {
+      base: '/store/baskets',
+      checkoutReview: '/store/baskets/checkout_review',
+      byId: (id) => '/store/baskets/' + id,
+      send: (id) => '/store/baskets/' + id + '/send',
+      submitDiscountCode: (id) => '/store/baskets/' + id + '/submit_discount_code',
+      removeDiscountCode: (id) => '/store/baskets/' + id + '/remove_discount_code'
+    }
+    this.CacheList = {
+      base: this.name + this.APIAdresses.base
+    }
+    this.restUrl = (id) => this.APIAdresses.base + '/' + id
+    /* Setting the callback functions for the CRUD operations. */
+    this.setCrudCallbacks({
+      get: (response) => { return new Basket(response.data) },
+      post: (response) => { return new Basket(response.data) },
+      put: (response) => { return new Basket(response.data) },
+      delete: (response) => { return response.data }
+    })
+  }
+
+  index(data) {
+    return this.sendRequest({
+      apiMethod: 'get',
+      api: this.api,
+      request: this.APIAdresses.base,
+      data: this.getNormalizedSendData({
+        category: null, // Number
+        per_page: 10, // Number
+        page: 1 // Number
+      }, data),
+      resolveCallback: (response) => {
+        const paginate = response.data
+        const results = response.data.results
+        delete paginate.results
+        return {
+          list: new BasketList(results),
+          paginate
+          // {
+          //   "count": 1,
+          //   "num_pages": 1,
+          //   "per_page": 10,
+          //   "current": 1,
+          //   "next": null,
+          //   "previous": null,
+          // }
+        }
+      },
+      rejectCallback: (error) => {
+        return error
+      }
+    })
+  }
+
+  get (id) {
+    return this.sendRequest({
+      apiMethod: 'get',
+      api: this.api,
+      request: this.APIAdresses.byId(id),
+      resolveCallback: (response) => {
+        return new Basket(response.data)
+      },
+      rejectCallback: (error) => {
+        return error
+      }
+    })
+  }
+
+  checkoutReview () {
+    return this.sendRequest({
+      apiMethod: 'get',
+      api: this.api,
+      request: this.APIAdresses.checkoutReview,
+      resolveCallback: (response) => {
+        return new Basket(response.data)
+      },
+      rejectCallback: (error) => {
+        return error
+      }
+    })
+  }
+
+  setAddress (basketId, addressData) {
+    return this.sendRequest({
+      apiMethod: 'put',
+      api: this.api,
+      request: this.APIAdresses.byId(basketId),
+      data: this.getNormalizedSendData({
+        province: null, // String
+        city: null, // String
+        address: null, // String
+        postal_code: null // String
+      }, addressData),
+      resolveCallback: (response) => {
+        return new Basket(response.data)
+      },
+      rejectCallback: (error) => {
+        return error
+      }
+    })
+  }
+
+  submitDiscountCode (basketId, code) {
+    return this.sendRequest({
+      apiMethod: 'put',
+      api: this.api,
+      request: this.APIAdresses.submitDiscountCode(basketId),
+      data: this.getNormalizedSendData({
+        code: null // String
+      }, { code }),
+      resolveCallback: (response) => {
+        return new Basket(response.data)
+      },
+      rejectCallback: (error) => {
+        return error
+      }
+    })
+  }
+
+  removeDiscountCode (basketId) {
+    return this.sendRequest({
+      apiMethod: 'put',
+      api: this.api,
+      request: this.APIAdresses.removeDiscountCode(basketId),
+      resolveCallback: (response) => {
+        return new Basket(response.data)
+      },
+      rejectCallback: (error) => {
+        return error
+      }
+    })
+  }
+
+  send (basketId) {
+    return this.sendRequest({
+      apiMethod: 'put',
+      api: this.api,
+      request: this.APIAdresses.send(basketId),
+      resolveCallback: (response) => {
+        return new Basket(response.data)
+      },
+      rejectCallback: (error) => {
+        return error
+      }
+    })
+  }
+}

+ 132 - 0
src/api/models/BasketItem.js

@@ -0,0 +1,132 @@
+import { appApi } from 'src/boot/axios.js'
+import APIRepository from '../classes/APIRepository.js'
+import { BasketItem, BasketItemList } from 'src/models/BasketItem.js'
+
+export default class BasketItemAPI extends APIRepository {
+  constructor() {
+    super('package', appApi)
+    this.APIAdresses = {
+      base: '/store/items',
+      byId: (id) => '/store/items/' + id
+    }
+    this.CacheList = {
+      base: this.name + this.APIAdresses.base
+    }
+    this.restUrl = (id) => this.APIAdresses.base + '/' + id
+    /* Setting the callback functions for the CRUD operations. */
+    this.setCrudCallbacks({
+      get: (response) => { return new BasketItem(response.data) },
+      post: (response) => { return new BasketItem(response.data) },
+      put: (response) => { return new BasketItem(response.data) },
+      delete: (response) => { return response.data }
+    })
+  }
+
+  index(data) {
+    return this.sendRequest({
+      apiMethod: 'get',
+      api: this.api,
+      request: this.APIAdresses.base,
+      data: this.getNormalizedSendData({
+        category: null, // Number
+        per_page: 10, // Number
+        page: 1 // Number
+      }, data),
+      resolveCallback: (response) => {
+        const paginate = response.data
+        const results = response.data.results
+        delete paginate.results
+        return {
+          list: new BasketItemList(results),
+          paginate
+          // {
+          //   "count": 1,
+          //   "num_pages": 1,
+          //   "per_page": 10,
+          //   "current": 1,
+          //   "next": null,
+          //   "previous": null,
+          // }
+        }
+      },
+      rejectCallback: (error) => {
+        return error
+      }
+    })
+  }
+
+  get (id) {
+    return this.sendRequest({
+      apiMethod: 'get',
+      api: this.api,
+      request: this.APIAdresses.byId(id),
+      resolveCallback: (response) => {
+        return new BasketItem(response.data)
+      },
+      rejectCallback: (error) => {
+        return error
+      }
+    })
+  }
+
+  addProduct (productId, count) {
+    return this.sendRequest({
+      apiMethod: 'post',
+      api: this.api,
+      request: this.APIAdresses.base,
+      data: this.getNormalizedSendData({
+        product: null, // Number
+        count: null // Number
+      }, {
+        product: parseInt(productId),
+        count
+      }),
+      resolveCallback: (response) => {
+        return new BasketItem(response.data)
+      },
+      rejectCallback: (error) => {
+        return error
+      }
+    })
+  }
+
+  addPackage (packageId, count) {
+    return this.sendRequest({
+      apiMethod: 'post',
+      api: this.api,
+      request: this.APIAdresses.base,
+      data: this.getNormalizedSendData({
+        package: null, // Number
+        count: null // Number
+      }, {
+        package: packageId,
+        count
+      }),
+      resolveCallback: (response) => {
+        return new BasketItem(response.data)
+      },
+      rejectCallback: (error) => {
+        return error
+      }
+    })
+  }
+
+  update (basketItemId, count) {
+    return this.sendRequest({
+      apiMethod: 'put',
+      api: this.api,
+      request: this.APIAdresses.byId(basketItemId),
+      data: this.getNormalizedSendData({
+        count: null // Number
+      }, {
+        count
+      }),
+      resolveCallback: (response) => {
+        return new BasketItem(response.data)
+      },
+      rejectCallback: (error) => {
+        return error
+      }
+    })
+  }
+}

+ 18 - 0
src/components/Template/Header/MainHeader.vue

@@ -123,6 +123,15 @@
       </q-btn>
 
       <div class="left-side">
+        <q-btn v-if="user && user.id !== null"
+               flat
+               :to="{ name: 'Public.Cart' }"
+               class="btn-cart">
+          <q-badge>
+            0
+          </q-badge>
+          <q-icon name="shopping_cart" />
+        </q-btn>
         <q-btn v-if="user && user.id !== null"
                color="primary"
                @click="goToProperPanel">
@@ -529,6 +538,15 @@ export default {
         }
       }
     }
+    &.btn-cart {
+      background: white;
+      color: $primary;
+      margin-right: 20px;
+      .q-badge {
+        background: #FBF4EA;
+        color: $primary;
+      }
+    }
   }
 }
 </style>

+ 60 - 0
src/components/Widgets/Store/Cart/Cart.vue

@@ -0,0 +1,60 @@
+<template>
+  <div class="cart"
+       :style="localOptions.style">
+    <breadcrumbs class="q-mb-md" />
+    <div class="cart__stepper">
+      <stepper />
+    </div>
+  </div>
+</template>
+
+<script>
+import { mixinWidget } from 'src/mixin/Mixins.js'
+import Breadcrumbs from 'src/components/Widgets/Breadcrumbs/Breadcrumbs.vue'
+import Stepper from './components/stepper.vue'
+
+export default {
+  name: 'ContentList',
+  components: { Breadcrumbs, Stepper },
+  mixins: [mixinWidget],
+  data () {
+    return {
+      mounted: false
+    }
+  },
+  mounted() {
+    this.mounted = true
+    this.updateBreadcrumbs()
+  },
+  methods: {
+    updateBreadcrumbs () {
+      this.$store.commit('AppLayout/updateBreadcrumbs', {
+        visible: true,
+        loading: false,
+        path: [
+          {
+            label: 'خانه',
+            to: { name: 'Public.Home' }
+          },
+          {
+            label: 'فروشگاه',
+            to: { name: 'Public.Shop' }
+          },
+          {
+            label: 'سبد خرید',
+            to: { name: 'Public.Cart' }
+          }
+        ]
+      })
+    }
+  }
+}
+</script>
+
+<style scoped lang="scss">
+.cart {
+  .cart__stepper {
+    margin-top: 46px;
+  }
+}
+</style>

+ 21 - 0
src/components/Widgets/Store/Cart/OptionPanel.vue

@@ -0,0 +1,21 @@
+<template>
+  <option-panel-tabs v-model:options="localOptions">
+    <template #main-tab>
+      ...
+    </template>
+  </option-panel-tabs>
+</template>
+
+<script>
+import { defineComponent } from 'vue'
+import { mixinOptionPanel } from 'quasar-ui-q-page-builder'
+import OptionPanelTabs from 'quasar-ui-q-page-builder/src/components/OptionPanelComponents/OptionPanelTabs.vue'
+
+export default defineComponent({
+  name: 'OptionPanel',
+  components: {
+    OptionPanelTabs
+  },
+  mixins: [mixinOptionPanel]
+})
+</script>

+ 182 - 0
src/components/Widgets/Store/Cart/components/packageItem.vue

@@ -0,0 +1,182 @@
+<template>
+  <div class="package-item">
+    <div class="package-item__badges">
+      <div class="package-item__badge">
+        <div class="package-item__badge-title">
+          بسته
+        </div>
+        <div class="package-item__badge-icon">
+          <q-icon name="inventory_2" />
+        </div>
+      </div>
+    </div>
+    <div class="package-item__thumbnail">
+      <q-img :src="packageItem.thumbnail" />
+    </div>
+    <div class="package-item__title">
+      {{ packageItem.title }}
+    </div>
+    <div class="package-item__price">
+      <div class="package-item__price-base">
+        {{ parseInt(packageItem.unit_price).toLocaleString('fa') }}
+        تومان
+      </div>
+      <div class="package-item__price-final">
+        {{ packageItem.getFinalPrice() }}
+        تومان
+      </div>
+    </div>
+    <div class="package-item__actions">
+      <q-btn class="package-item__actions-view"
+             :to="{ name: 'Public.Package', params: { id: packageItem.id } }"
+             color="primary"
+             outline>
+        مشاهده جزییات
+      </q-btn>
+      <q-btn class="package-item__actions-add-to-cart"
+             :loading="addToCartLoading"
+             color="primary"
+             @click="addToCart">
+        افزودن به سبد
+      </q-btn>
+    </div>
+  </div>
+</template>
+
+<script>
+import { Package } from 'src/models/Package.js'
+import { APIGateway } from 'src/api/APIGateway.js'
+
+export default {
+  name: 'PackageItem',
+  props: {
+    packageItem: {
+      type: Package,
+      default: new Package()
+    }
+  },
+  data () {
+    return {
+      addToCartLoading: false
+    }
+  },
+  methods: {
+    addToCart () {
+      this.addToCartLoading = true
+      APIGateway.basketItem.addPackage(this.packageItem.id, 1)
+        .finally(() => {
+          this.addToCartLoading = false
+        })
+    }
+  }
+}
+</script>
+
+<style scoped lang="scss">
+.package-item {
+  $transition-time: 0.2s;
+  background: #F6F6F6;
+  border: 1px solid #DFE1EC;
+  border-radius: 8px;
+  position: relative;
+  .package-item__badges {
+    position: absolute;
+    top: 12px;
+    right: 12px;
+    display: flex;
+    flex-flow: column;
+    align-items: flex-end;
+    gap: 10px;
+    .package-item__badge {
+      transition: all $transition-time;
+      width: max-content;
+      background: #FFFFFF;
+      border: 1px solid #DFE1EC;
+      border-radius: 20px;
+      height: 30px;
+      padding: 4px 8px;
+      text-align: center;
+      display: flex;
+      justify-content: space-between;
+      gap: 4px;
+      align-items: center;
+      .package-item__badge-title {
+        display: none;
+        font-size: 13px;
+        color: #212121;
+      }
+      .package-item__badge-icon {
+        color: $primary;
+      }
+    }
+  }
+  :deep(.package-item__thumbnail) {
+    transition: all $transition-time;
+    margin: 15px 58px 12px 58px;
+    .q-img {
+      width: 100%;
+    }
+  }
+  .package-item__title {
+    color: #212121;
+    width: 100%;
+    padding: 0 4px;
+    text-align: center;
+    font-size: 18px;
+    margin-bottom: 52px;
+  }
+  .package-item__price {
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+    padding: 0 20px;
+    margin-bottom: 43px;
+    .package-item__price-final {
+      font-size: 20px;
+      color: #475F4A;
+    }
+    .package-item__price-base {
+      font-size: 16px;
+      color: #FF3D3D;
+      text-decoration: line-through;
+    }
+  }
+  .package-item__actions {
+    display: none;
+    margin: 0 25px 25px 25px;
+    .package-item__actions-view {
+      background: white;
+      margin-bottom: 20px;
+      width: 100%;
+    }
+    .package-item__actions-add-to-cart {
+      width: 100%;
+    }
+  }
+  &:hover {
+    .package-item__badges {
+      .package-item__badge {
+        .package-item__badge-title {
+          display: block;
+        }
+      }
+    }
+    :deep(.package-item__thumbnail) {
+      margin: 10px 105px 4px 105px;
+      .q-img {
+        width: 100%;
+      }
+    }
+    .package-item__title {
+      margin-bottom: 44px;
+    }
+    .package-item__price {
+      margin-bottom: 27px;
+    }
+    .package-item__actions {
+      display: flex;
+      flex-flow: column;
+    }
+  }
+}
+</style>

+ 190 - 0
src/components/Widgets/Store/Cart/components/productItem.vue

@@ -0,0 +1,190 @@
+<template>
+  <div class="product-item">
+    <div class="product-item__badges">
+      <div class="product-item__badge">
+        <div class="product-item__badge-title">
+          {{ product.is_physical_info.label }}
+        </div>
+        <div class="product-item__badge-icon">
+          <q-icon :name="product.is_physical ? 'shopping_basket' : 'folder'" />
+        </div>
+      </div>
+      <div class="product-item__badge">
+        <div class="product-item__badge-title">
+          {{ product.physical_type_info.label }}
+        </div>
+        <div class="product-item__badge-icon">
+          <q-icon name="category" />
+        </div>
+      </div>
+    </div>
+    <div class="product-item__thumbnail">
+      <q-img :src="product.thumbnail" />
+    </div>
+    <div class="product-item__title">
+      {{ product.title }}
+    </div>
+    <div class="product-item__price">
+      <div class="product-item__price-base">
+        {{ parseInt(product.unit_price).toLocaleString('fa') }}
+        تومان
+      </div>
+      <div class="product-item__price-final">
+        {{ product.getFinalPrice() }}
+        تومان
+      </div>
+    </div>
+    <div class="product-item__actions">
+      <q-btn class="product-item__actions-view"
+             color="primary"
+             :to="{ name: 'Public.Product', params: { id: product.id } }"
+             outline>
+        مشاهده جزییات
+      </q-btn>
+      <q-btn class="product-item__actions-add-to-cart"
+             :loading="addToCartLoading"
+             color="primary"
+             @click="addToCart">
+        افزودن به سبد
+      </q-btn>
+    </div>
+  </div>
+</template>
+
+<script>
+import { Product } from 'src/models/Product.js'
+import { APIGateway } from 'src/api/APIGateway.js'
+
+export default {
+  name: 'ProductItem',
+  props: {
+    product: {
+      type: Product,
+      default: new Product()
+    }
+  },
+  data () {
+    return {
+      addToCartLoading: false
+    }
+  },
+  methods: {
+    addToCart () {
+      this.addToCartLoading = true
+      APIGateway.basketItem.addProduct(this.product.id, 1)
+        .finally(() => {
+          this.addToCartLoading = false
+        })
+    }
+  }
+}
+</script>
+
+<style scoped lang="scss">
+.product-item {
+  $transition-time: 0.2s;
+  background: #F6F6F6;
+  border: 1px solid #DFE1EC;
+  border-radius: 8px;
+  position: relative;
+  .product-item__badges {
+    position: absolute;
+    top: 12px;
+    right: 12px;
+    display: flex;
+    flex-flow: column;
+    align-items: flex-end;
+    gap: 10px;
+    .product-item__badge {
+      transition: all $transition-time;
+      width: max-content;
+      background: #FFFFFF;
+      border: 1px solid #DFE1EC;
+      border-radius: 20px;
+      height: 30px;
+      padding: 4px 8px;
+      text-align: center;
+      display: flex;
+      justify-content: space-between;
+      gap: 4px;
+      align-items: center;
+      .product-item__badge-title {
+        display: none;
+        font-size: 13px;
+        color: #212121;
+      }
+      .product-item__badge-icon {
+        color: $primary;
+      }
+    }
+  }
+  :deep(.product-item__thumbnail) {
+    transition: all $transition-time;
+    margin: 15px 58px 12px 58px;
+    .q-img {
+      width: 100%;
+    }
+  }
+  .product-item__title {
+    color: #212121;
+    width: 100%;
+    padding: 0 4px;
+    text-align: center;
+    font-size: 18px;
+    margin-bottom: 52px;
+  }
+  .product-item__price {
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+    padding: 0 20px;
+    margin-bottom: 43px;
+    .product-item__price-final {
+      font-size: 20px;
+      color: #475F4A;
+    }
+    .product-item__price-base {
+      font-size: 16px;
+      color: #FF3D3D;
+      text-decoration: line-through;
+    }
+  }
+  .product-item__actions {
+    display: none;
+    margin: 0 25px 25px 25px;
+    .product-item__actions-view {
+      background: white;
+      margin-bottom: 20px;
+      width: 100%;
+    }
+    .product-item__actions-add-to-cart {
+      width: 100%;
+    }
+  }
+  &:hover {
+    .product-item__badges {
+      .product-item__badge {
+        .product-item__badge-title {
+          display: block;
+        }
+      }
+    }
+    :deep(.product-item__thumbnail) {
+      margin: 10px 105px 4px 105px;
+      .q-img {
+        width: 100%;
+      }
+    }
+    .product-item__title {
+      margin-bottom: 44px;
+    }
+    .product-item__price {
+      margin-bottom: 27px;
+    }
+    .product-item__actions {
+      display: flex;
+      flex-flow: column;
+    }
+  }
+}
+</style>

+ 308 - 0
src/components/Widgets/Store/Cart/components/step1/cartItem.vue

@@ -0,0 +1,308 @@
+<template>
+  <div class="cart-item">
+    <div class="cart-item__use-type">
+      <q-icon :name="usageIcon" />
+    </div>
+    <div class="cart-item__remove-action">
+      <q-btn flat
+             icon="close"
+             @click="onRemove" />
+    </div>
+    <div class="cart-item__thumbnail">
+      <q-img :src="packageOrProductObject.thumbnail" />
+    </div>
+    <div class="cart-item__info">
+      <div class="cart-item__title">
+        {{ packageOrProductObject.title }}
+      </div>
+      <div class="cart-item__attrs">
+        <div class="cart-item__attr">
+          <div class="cart-item__attr-label">
+            نوع:
+          </div>
+          <div class="cart-item__attr-value">
+            {{ packageOrProductObject.is_physical_info.label }}
+          </div>
+        </div>
+        <div v-if="basketItem.product"
+             class="cart-item__attr">
+          <div class="cart-item__attr-label">
+            جنس:
+          </div>
+          <div class="cart-item__attr-value">
+            {{ packageOrProductObject.physical_type_info.label }}
+          </div>
+        </div>
+      </div>
+      <div v-if="false"
+           class="cart-item__type-info">
+        صوت
+        <q-icon color="primary"
+                name="account_balance_wallet" />
+      </div>
+      <div class="cart-item__count-action">
+        <div class="cart-item__count-action-increase">
+          <q-btn outline
+                 color="primary"
+                 icon="add"
+                 :loading="basketItem.loading"
+                 @click="onIncrease" />
+        </div>
+        <div class="cart-item__count-action-count">
+          {{ basketItem.count }}
+        </div>
+        <div class="cart-item__count-action-decrease">
+          <q-btn v-if="basketItem.count > 1"
+                 outline
+                 color="red"
+                 icon="remove"
+                 :loading="basketItem.loading"
+                 @click="onDecrease" />
+          <q-btn v-else
+                 flat
+                 color="red"
+                 icon="delete_outline"
+                 :loading="basketItem.loading"
+                 @click="onDecrease" />
+        </div>
+      </div>
+      <div class="cart-item__price">
+        <div class="cart-item__price-base">
+          {{ packageOrProductObject.unit_price.toLocaleString('fa') }}
+          تومان
+        </div>
+        <div class="cart-item__price-final">
+          {{ packageOrProductObject.getFinalPrice() }}
+          تومان
+        </div>
+      </div>
+      <div v-if="basketItem.package"
+           class="cart-item__products">
+        <div v-for="(product, productIndex) in basketItem.package_info.products_info.list"
+             :key="productIndex"
+             class="cart-item__product">
+          {{ product.title }}
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+import { BasketItem } from 'src/models/BasketItem.js'
+import { Product } from 'src/models/Product'
+
+export default {
+  name: 'cartItem',
+  props: {
+    basketItem: {
+      type: BasketItem,
+      default: new BasketItem()
+    }
+  },
+  emits: ['increase', 'decrease', 'remove'],
+  data () {
+    return {
+      step: 1
+    }
+  },
+  computed: {
+    packageOrProductObject () {
+      if (this.basketItem.product) {
+        return this.basketItem.product_info
+      }
+      if (this.basketItem.package) {
+        return this.basketItem.package_info
+      }
+
+      return new Product()
+    },
+    usageIcon () {
+      if (this.basketItem.package) {
+        return 'inventory_2'
+      }
+      if (this.packageOrProductObject.is_physical) {
+        return 'local_shipping'
+      }
+      return 'file_download'
+    }
+  },
+  methods: {
+    onIncrease () {
+      this.$emit('increase', this.basketItem)
+    },
+    onDecrease () {
+      this.$emit('decrease', this.basketItem)
+    },
+    onRemove () {
+      this.$emit('remove', this.basketItem)
+    }
+  }
+}
+</script>
+
+<style scoped lang="scss">
+.cart-item {
+  $cart-background-color: #F9F9FB;
+  $cart-item__thumbnail-width: 153px;
+  position: relative;
+  background: $cart-background-color;
+  border: 1px solid #DFE1EC;
+  border-radius: 8px;
+  padding: 24px 22px 24px 67px;
+  display: flex;
+  flex-flow: row;
+  gap: 30px;
+  min-height: 203px;
+  .cart-item__use-type {
+    $cart-item__use-type-width: 37px;
+    position: absolute;
+    top: 0;
+    left: 16px;
+    width: $cart-item__use-type-width;
+    color: white;
+    text-align: center;
+    padding-top: 10px;
+    padding-bottom: 22px;
+    background: $primary;
+    &:after {
+      position: absolute;
+      content: ' ';
+      bottom: calc( (#{$cart-item__use-type-width} * -1 / 2) - 9px );
+      left: 0;
+      transform: rotate(45deg);
+      background: $cart-background-color;
+      width: $cart-item__use-type-width;
+      height: $cart-item__use-type-width;
+      border-radius: 2px;
+    }
+  }
+  .cart-item__remove-action {
+    position: absolute;
+    top: 85px;
+    left: 20px;
+    .q-btn {
+      width: 32px;
+      height: 32px;
+    }
+  }
+  .cart-item__thumbnail {
+    width: $cart-item__thumbnail-width;
+  }
+  .cart-item__info {
+    width: calc( 100% - #{$cart-item__thumbnail-width} );
+    color: #212121;
+    .cart-item__title {
+      font-size: 22px;
+      margin-bottom: 27px;
+    }
+    .cart-item__attrs {
+      display: flex;
+      flex-flow: row;
+      gap: 35px;
+      flex-wrap: nowrap;
+      margin-bottom: 70px;
+      .cart-item__attr {
+        display: flex;
+        flex-flow: row;
+        gap: 5px;
+        flex-wrap: nowrap;
+        .cart-item__attr-label {
+          font-size: 16px;
+          color: #797979;
+          &:before {
+            display: inline-block;
+            content: ' ';
+            width: 8px;
+            height: 8px;
+            background: #475F4A;
+            border-radius: 100%;
+            margin-right: 4px;
+          }
+        }
+        .cart-item__attr-value {
+          font-size: 16px;
+        }
+      }
+    }
+    .cart-item__type-info {
+      position: absolute;
+      top: 30px;
+      right: 22px;
+      display: flex;
+      flex-flow: row;
+      align-items: center;
+      justify-content: space-between;
+      gap: 2px;
+      padding: 0 4px 0 16px;
+      background: #FFFFFF;
+      border: 1px solid #DFE1EC;
+      border-radius: 20px;
+      color: #212121;
+      font-size: 16px;
+    }
+    .cart-item__count-action {
+      position: absolute;
+      right: 20px;
+      top: 65px;
+      background: #FFFFFF;
+      border: 1px solid #DFE1EC;
+      border-radius: 8px;
+      padding: 12px 10px;
+      display: flex;
+      flex-flow: row;
+      justify-content: space-between;
+      align-items: center;
+      width: 120px;
+      $btn-size: 24px;
+      .q-btn {
+        width: $btn-size;
+        height: $btn-size;
+        min-height: $btn-size;
+        padding: 0;
+      }
+      .cart-item__count-action-count {
+        color: $primary;
+        font-size: 16px;
+      }
+    }
+    .cart-item__price {
+      position: absolute;
+      top: 136px;
+      right: 20px;
+      display: flex;
+      flex-flow: row;
+      align-items: center;
+      justify-content: space-between;
+      gap: 19px;
+      .cart-item__price-base {
+        color: #FF3D3D;
+        font-size: 18px;
+        text-decoration: line-through;
+      }
+      .cart-item__price-final {
+        color: $primary;
+        font-size: 24px;
+      }
+    }
+    .cart-item__products {
+      display: flex;
+      flex-flow: row;
+      flex-wrap: wrap;
+      .cart-item__product {
+        width: 33.33%;
+        margin-bottom: 17px;
+        &:before {
+          display: inline-block;
+          content: ' ';
+          width: 8px;
+          height: 8px;
+          background: #475F4A;
+          border-radius: 100%;
+          margin-right: 4px;
+        }
+      }
+    }
+  }
+}
+</style>

+ 149 - 0
src/components/Widgets/Store/Cart/components/step1/sidebar.vue

@@ -0,0 +1,149 @@
+<template>
+  <div class="step1__sidebar">
+    <div class="step1__sidebar-total-price">
+      <div class="step1__sidebar-total-price-label">
+        قیمت کالاها
+      </div>
+      <div class="step1__sidebar-total-price-value">
+        {{ basket.order_price.toLocaleString('fa') }}
+        تومان
+      </div>
+    </div>
+    <div class="step1__sidebar-total-discount">
+      <div class="step1__sidebar-total-discount-label">
+        تخفیف ها
+      </div>
+      <div class="step1__sidebar-total-discount-value">
+        {{ basket.overall_discount_amount.toLocaleString('fa') }}
+        تومان
+      </div>
+    </div>
+    <div v-if="basket.discount"
+         class="step1__sidebar-discount-code">
+      <div class="step1__sidebar-discount-code-label">
+        کد تخفیف
+      </div>
+      <div class="step1__sidebar-discount-code-value">
+        {{ basket.discount_info.code }}
+      </div>
+    </div>
+    <div class="step1__sidebar-separator" />
+    <div class="step1__sidebar-final-price">
+      <div class="step1__sidebar-final-price-label">
+        قیمت کالاها
+      </div>
+      <div class="step1__sidebar-final-price-value">
+        {{ basket.sellable_price.toLocaleString('fa') }}
+        تومان
+      </div>
+    </div>
+    <div class="step1__sidebar-continue-action">
+      <q-btn color="primary"
+             unelevated
+             @click="onComplete">
+        به خرید ادامه بده
+      </q-btn>
+    </div>
+  </div>
+</template>
+
+<script>
+import { Basket } from 'src/models/Basket.js'
+
+export default {
+  name: 'cartItem',
+  props: {
+    basket: {
+      type: Basket,
+      default: new Basket()
+    }
+  },
+  emits: ['complete'],
+  data () {
+    return {
+      step: 1
+    }
+  },
+  methods: {
+    onComplete () {
+      this.$emit('complete')
+    }
+  }
+}
+</script>
+
+<style scoped lang="scss">
+.step1__sidebar {
+  width: 100%;
+  background: #F9F9FB;
+  border: 1px solid #DFE1EC;
+  border-radius: 8px;
+  padding: 34px 16px 30px 16px;
+  .step1__sidebar-total-price {
+    display: flex;
+    justify-content: space-between;
+    margin-bottom: 26px;
+    letter-spacing: 0;
+    .step1__sidebar-total-price-label {
+      color: #212121;
+      font-size: 18px;
+    }
+    .step1__sidebar-total-price-value {
+      color: $primary;
+      font-size: 18px;
+    }
+  }
+  .step1__sidebar-total-discount {
+    display: flex;
+    justify-content: space-between;
+    margin-bottom: 26px;
+    letter-spacing: 0;
+    color: #FF3D3D;
+    .step1__sidebar-total-discount-label {
+      font-size: 18px;
+    }
+    .step1__sidebar-total-discount-value {
+      font-size: 18px;
+    }
+  }
+  .step1__sidebar-discount-code {
+    display: flex;
+    justify-content: space-between;
+    margin-bottom: 22px;
+    letter-spacing: 0;
+    color: #FF3D3D;
+    .step1__sidebar-discount-code-label {
+      font-size: 18px;
+    }
+    .step1__sidebar-discount-code-value {
+      font-size: 18px;
+    }
+  }
+  .step1__sidebar-separator {
+    width: 100%;
+    background: #F9F9FB;
+    border: 1px dashed $primary;
+    margin-bottom: 19px;
+  }
+  .step1__sidebar-final-price {
+    display: flex;
+    justify-content: space-between;
+    margin-bottom: 40px;
+    letter-spacing: 0;
+    .step1__sidebar-final-price-label {
+      color: #212121;
+      font-size: 18px;
+    }
+    .step1__sidebar-final-price-value {
+      font-size: 20px;
+      color: $primary;
+    }
+  }
+  .step1__sidebar-continue-action {
+    padding: 0 16px;
+    .q-btn {
+      width: 100%;
+    }
+  }
+}
+</style>

+ 116 - 0
src/components/Widgets/Store/Cart/components/step1/step1.vue

@@ -0,0 +1,116 @@
+<template>
+  <div class="step1">
+    <q-linear-progress v-if="basket.loading"
+                       indeterminate />
+    <template v-if="!basket.loading">
+      <div class="step1__cart-items">
+        <cart-item v-for="(basketItem, basketItemIndex) in basket.items_info.list"
+                   :key="basketItemIndex"
+                   :basket-item="basketItem"
+                   class="step1__cart-item"
+                   @remove="onRemove(basketItem)"
+                   @increase="onIncrease(basketItem)"
+                   @decrease="onDecrease(basketItem)" />
+      </div>
+      <div class="step1__sidebar">
+        <sidebar :basket="basket"
+                 @complete="onComplete" />
+      </div>
+    </template>
+  </div>
+</template>
+
+<script>
+import Sidebar from './sidebar.vue'
+import CartItem from './cartItem.vue'
+import { Basket } from 'src/models/Basket.js'
+import { APIGateway } from 'src/api/APIGateway.js'
+
+export default {
+  name: 'step1',
+  components: { CartItem, Sidebar },
+  props: {
+    type: {
+      type: String,
+      default: 'product'
+    }
+  },
+  emits: ['complete'],
+  data () {
+    return {
+      basket: new Basket()
+    }
+  },
+  mounted() {
+    this.checkoutReview()
+  },
+  methods: {
+    checkoutReview () {
+      this.basket.loading = true
+      APIGateway.basket.checkoutReview()
+        .then((basket) => {
+          this.basket = new Basket(basket)
+        })
+        .finally(() => {
+          this.basket.loading = false
+        })
+    },
+    onIncrease (basketItem) {
+      basketItem.loading = true
+      const newCount = basketItem.count + 1
+      APIGateway.basketItem.update(basketItem.id, newCount)
+        .then(() => {
+          this.checkoutReview()
+        })
+        .finally(() => {
+          basketItem.loading = false
+        })
+    },
+    onDecrease (basketItem) {
+      basketItem.loading = true
+      const newCount = basketItem.count - 1
+      APIGateway.basketItem.update(basketItem.id, newCount)
+        .then(() => {
+          this.checkoutReview()
+        })
+        .finally(() => {
+          basketItem.loading = false
+        })
+    },
+    onRemove (basketItem) {
+      basketItem.loading = true
+      APIGateway.basketItem.update(basketItem.id, 0)
+        .then(() => {
+          this.checkoutReview()
+        })
+        .finally(() => {
+          basketItem.loading = false
+        })
+    },
+    onComplete () {
+      this.$emit('complete')
+    }
+  }
+}
+</script>
+
+<style scoped lang="scss">
+.step1 {
+  display: flex;
+  flex-flow: row;
+  gap: 28px;
+  $sidebar-width: 312px;
+  .step1__cart-items {
+    width: calc( 100% - #{$sidebar-width} );
+    display: flex;
+    flex-flow: column;
+    gap: 24px;
+    .step1__cart-item {
+
+    }
+  }
+  .step1__sidebar {
+    width: $sidebar-width;
+  }
+}
+</style>

+ 308 - 0
src/components/Widgets/Store/Cart/components/step2/cartItem.vue

@@ -0,0 +1,308 @@
+<template>
+  <div class="cart-item">
+    <div class="cart-item__use-type">
+      <q-icon :name="usageIcon" />
+    </div>
+    <div class="cart-item__remove-action">
+      <q-btn flat
+             icon="close"
+             @click="onRemove" />
+    </div>
+    <div class="cart-item__thumbnail">
+      <q-img :src="packageOrProductObject.thumbnail" />
+    </div>
+    <div class="cart-item__info">
+      <div class="cart-item__title">
+        {{ packageOrProductObject.title }}
+      </div>
+      <div class="cart-item__attrs">
+        <div class="cart-item__attr">
+          <div class="cart-item__attr-label">
+            نوع:
+          </div>
+          <div class="cart-item__attr-value">
+            {{ packageOrProductObject.is_physical_info.label }}
+          </div>
+        </div>
+        <div v-if="basketItem.product"
+             class="cart-item__attr">
+          <div class="cart-item__attr-label">
+            جنس:
+          </div>
+          <div class="cart-item__attr-value">
+            {{ packageOrProductObject.physical_type_info.label }}
+          </div>
+        </div>
+      </div>
+      <div v-if="false"
+           class="cart-item__type-info">
+        صوت
+        <q-icon color="primary"
+                name="account_balance_wallet" />
+      </div>
+      <div class="cart-item__count-action">
+        <div class="cart-item__count-action-increase">
+          <q-btn outline
+                 color="primary"
+                 icon="add"
+                 :loading="basketItem.loading"
+                 @click="onIncrease" />
+        </div>
+        <div class="cart-item__count-action-count">
+          {{ basketItem.count }}
+        </div>
+        <div class="cart-item__count-action-decrease">
+          <q-btn v-if="basketItem.count > 1"
+                 outline
+                 color="red"
+                 icon="remove"
+                 :loading="basketItem.loading"
+                 @click="onDecrease" />
+          <q-btn v-else
+                 flat
+                 color="red"
+                 icon="delete_outline"
+                 :loading="basketItem.loading"
+                 @click="onDecrease" />
+        </div>
+      </div>
+      <div class="cart-item__price">
+        <div class="cart-item__price-base">
+          {{ packageOrProductObject.unit_price.toLocaleString('fa') }}
+          تومان
+        </div>
+        <div class="cart-item__price-final">
+          {{ packageOrProductObject.getFinalPrice() }}
+          تومان
+        </div>
+      </div>
+      <div v-if="basketItem.package"
+           class="cart-item__products">
+        <div v-for="(product, productIndex) in basketItem.package_info.products_info.list"
+             :key="productIndex"
+             class="cart-item__product">
+          {{ product.title }}
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+import { BasketItem } from 'src/models/BasketItem.js'
+import { Product } from 'src/models/Product'
+
+export default {
+  name: 'cartItem',
+  props: {
+    basketItem: {
+      type: BasketItem,
+      default: new BasketItem()
+    }
+  },
+  emits: ['increase', 'decrease', 'remove'],
+  data () {
+    return {
+      step: 1
+    }
+  },
+  computed: {
+    packageOrProductObject () {
+      if (this.basketItem.product) {
+        return this.basketItem.product_info
+      }
+      if (this.basketItem.package) {
+        return this.basketItem.package_info
+      }
+
+      return new Product()
+    },
+    usageIcon () {
+      if (this.basketItem.package) {
+        return 'inventory_2'
+      }
+      if (this.packageOrProductObject.is_physical) {
+        return 'local_shipping'
+      }
+      return 'file_download'
+    }
+  },
+  methods: {
+    onIncrease () {
+      this.$emit('increase', this.basketItem)
+    },
+    onDecrease () {
+      this.$emit('decrease', this.basketItem)
+    },
+    onRemove () {
+      this.$emit('remove', this.basketItem)
+    }
+  }
+}
+</script>
+
+<style scoped lang="scss">
+.cart-item {
+  $cart-background-color: #F9F9FB;
+  $cart-item__thumbnail-width: 153px;
+  position: relative;
+  background: $cart-background-color;
+  border: 1px solid #DFE1EC;
+  border-radius: 8px;
+  padding: 24px 22px 24px 67px;
+  display: flex;
+  flex-flow: row;
+  gap: 30px;
+  min-height: 203px;
+  .cart-item__use-type {
+    $cart-item__use-type-width: 37px;
+    position: absolute;
+    top: 0;
+    left: 16px;
+    width: $cart-item__use-type-width;
+    color: white;
+    text-align: center;
+    padding-top: 10px;
+    padding-bottom: 22px;
+    background: $primary;
+    &:after {
+      position: absolute;
+      content: ' ';
+      bottom: calc( (#{$cart-item__use-type-width} * -1 / 2) - 9px );
+      left: 0;
+      transform: rotate(45deg);
+      background: $cart-background-color;
+      width: $cart-item__use-type-width;
+      height: $cart-item__use-type-width;
+      border-radius: 2px;
+    }
+  }
+  .cart-item__remove-action {
+    position: absolute;
+    top: 85px;
+    left: 20px;
+    .q-btn {
+      width: 32px;
+      height: 32px;
+    }
+  }
+  .cart-item__thumbnail {
+    width: $cart-item__thumbnail-width;
+  }
+  .cart-item__info {
+    width: calc( 100% - #{$cart-item__thumbnail-width} );
+    color: #212121;
+    .cart-item__title {
+      font-size: 22px;
+      margin-bottom: 27px;
+    }
+    .cart-item__attrs {
+      display: flex;
+      flex-flow: row;
+      gap: 35px;
+      flex-wrap: nowrap;
+      margin-bottom: 70px;
+      .cart-item__attr {
+        display: flex;
+        flex-flow: row;
+        gap: 5px;
+        flex-wrap: nowrap;
+        .cart-item__attr-label {
+          font-size: 16px;
+          color: #797979;
+          &:before {
+            display: inline-block;
+            content: ' ';
+            width: 8px;
+            height: 8px;
+            background: #475F4A;
+            border-radius: 100%;
+            margin-right: 4px;
+          }
+        }
+        .cart-item__attr-value {
+          font-size: 16px;
+        }
+      }
+    }
+    .cart-item__type-info {
+      position: absolute;
+      top: 30px;
+      right: 22px;
+      display: flex;
+      flex-flow: row;
+      align-items: center;
+      justify-content: space-between;
+      gap: 2px;
+      padding: 0 4px 0 16px;
+      background: #FFFFFF;
+      border: 1px solid #DFE1EC;
+      border-radius: 20px;
+      color: #212121;
+      font-size: 16px;
+    }
+    .cart-item__count-action {
+      position: absolute;
+      right: 20px;
+      top: 65px;
+      background: #FFFFFF;
+      border: 1px solid #DFE1EC;
+      border-radius: 8px;
+      padding: 12px 10px;
+      display: flex;
+      flex-flow: row;
+      justify-content: space-between;
+      align-items: center;
+      width: 120px;
+      $btn-size: 24px;
+      .q-btn {
+        width: $btn-size;
+        height: $btn-size;
+        min-height: $btn-size;
+        padding: 0;
+      }
+      .cart-item__count-action-count {
+        color: $primary;
+        font-size: 16px;
+      }
+    }
+    .cart-item__price {
+      position: absolute;
+      top: 136px;
+      right: 20px;
+      display: flex;
+      flex-flow: row;
+      align-items: center;
+      justify-content: space-between;
+      gap: 19px;
+      .cart-item__price-base {
+        color: #FF3D3D;
+        font-size: 18px;
+        text-decoration: line-through;
+      }
+      .cart-item__price-final {
+        color: $primary;
+        font-size: 24px;
+      }
+    }
+    .cart-item__products {
+      display: flex;
+      flex-flow: row;
+      flex-wrap: wrap;
+      .cart-item__product {
+        width: 33.33%;
+        margin-bottom: 17px;
+        &:before {
+          display: inline-block;
+          content: ' ';
+          width: 8px;
+          height: 8px;
+          background: #475F4A;
+          border-radius: 100%;
+          margin-right: 4px;
+        }
+      }
+    }
+  }
+}
+</style>

+ 149 - 0
src/components/Widgets/Store/Cart/components/step2/sidebar.vue

@@ -0,0 +1,149 @@
+<template>
+  <div class="step2__sidebar">
+    <div class="step2__sidebar-total-price">
+      <div class="step2__sidebar-total-price-label">
+        قیمت کالاها
+      </div>
+      <div class="step2__sidebar-total-price-value">
+        {{ basket.order_price.toLocaleString('fa') }}
+        تومان
+      </div>
+    </div>
+    <div class="step2__sidebar-delivery-cost">
+      <div class="step2__sidebar-delivery-cost-label">
+        هزینه ارسال
+      </div>
+      <div class="step2__sidebar-delivery-cost-value">
+        {{ basket.delivery_cost.toLocaleString('fa') }}
+        تومان
+      </div>
+    </div>
+    <div class="step2__sidebar-overall-discount-amount">
+      <div class="step2__sidebar-overall-discount-amount-label">
+        تخفیف ها
+      </div>
+      <div class="step2__sidebar-overall-discount-amount-value">
+        {{ basket.overall_discount_amount.toLocaleString('fa') }}
+      </div>
+    </div>
+    <div class="step2__sidebar-separator" />
+    <div class="step2__sidebar-final-price">
+      <div class="step2__sidebar-final-price-label">
+        جمع
+      </div>
+      <div class="step2__sidebar-final-price-value">
+        {{ basket.overall_order_price.toLocaleString('fa') }}
+        تومان
+      </div>
+    </div>
+    <div class="step2__sidebar-continue-action">
+      <q-btn color="primary"
+             unelevated
+             @click="onComplete">
+        ثبت و ادامه خرید
+      </q-btn>
+    </div>
+  </div>
+</template>
+
+<script>
+import { Basket } from 'src/models/Basket.js'
+
+export default {
+  name: 'cartItem',
+  props: {
+    basket: {
+      type: Basket,
+      default: new Basket()
+    }
+  },
+  emits: ['complete'],
+  data () {
+    return {
+      step: 1
+    }
+  },
+  methods: {
+    onComplete () {
+      this.$emit('complete')
+    }
+  }
+}
+</script>
+
+<style scoped lang="scss">
+.step2__sidebar {
+  width: 100%;
+  background: #F9F9FB;
+  border: 1px solid #DFE1EC;
+  border-radius: 8px;
+  padding: 34px 16px 30px 16px;
+  .step2__sidebar-total-price {
+    display: flex;
+    justify-content: space-between;
+    margin-bottom: 26px;
+    letter-spacing: 0;
+    .step2__sidebar-total-price-label {
+      color: #212121;
+      font-size: 18px;
+    }
+    .step2__sidebar-total-price-value {
+      color: $primary;
+      font-size: 18px;
+    }
+  }
+  .step2__sidebar-delivery-cost {
+    display: flex;
+    justify-content: space-between;
+    margin-bottom: 26px;
+    letter-spacing: 0;
+    .step2__sidebar-delivery-cost-label {
+      color: #212121;
+      font-size: 18px;
+    }
+    .step2__sidebar-delivery-cost-value {
+      color: $primary;
+      font-size: 18px;
+    }
+  }
+  .step2__sidebar-overall-discount-amount {
+    display: flex;
+    justify-content: space-between;
+    margin-bottom: 22px;
+    letter-spacing: 0;
+    color: #FF3D3D;
+    .step2__sidebar-overall-discount-amount-label {
+      font-size: 18px;
+    }
+    .step2__sidebar-overall-discount-amount-value {
+      font-size: 18px;
+    }
+  }
+  .step2__sidebar-separator {
+    width: 100%;
+    background: #F9F9FB;
+    border: 1px dashed $primary;
+    margin-bottom: 19px;
+  }
+  .step2__sidebar-final-price {
+    display: flex;
+    justify-content: space-between;
+    margin-bottom: 40px;
+    letter-spacing: 0;
+    .step2__sidebar-final-price-label {
+      color: #212121;
+      font-size: 18px;
+    }
+    .step2__sidebar-final-price-value {
+      font-size: 20px;
+      color: $primary;
+    }
+  }
+  .step2__sidebar-continue-action {
+    padding: 0 16px;
+    .q-btn {
+      width: 100%;
+    }
+  }
+}
+</style>

+ 116 - 0
src/components/Widgets/Store/Cart/components/step2/step2.vue

@@ -0,0 +1,116 @@
+<template>
+  <div class="step1">
+    <q-linear-progress v-if="basket.loading"
+                       indeterminate />
+    <template v-if="!basket.loading">
+      <div class="step1__cart-items">
+        <cart-item v-for="(basketItem, basketItemIndex) in basket.items_info.list"
+                   :key="basketItemIndex"
+                   :basket-item="basketItem"
+                   class="step1__cart-item"
+                   @remove="onRemove(basketItem)"
+                   @increase="onIncrease(basketItem)"
+                   @decrease="onDecrease(basketItem)" />
+      </div>
+      <div class="step1__sidebar">
+        <sidebar :basket="basket"
+                 @complete="onComplete" />
+      </div>
+    </template>
+  </div>
+</template>
+
+<script>
+import Sidebar from './sidebar.vue'
+import CartItem from './cartItem.vue'
+import { Basket } from 'src/models/Basket.js'
+import { APIGateway } from 'src/api/APIGateway.js'
+
+export default {
+  name: 'step1',
+  components: { CartItem, Sidebar },
+  props: {
+    type: {
+      type: String,
+      default: 'product'
+    }
+  },
+  emits: ['complete'],
+  data () {
+    return {
+      basket: new Basket()
+    }
+  },
+  mounted() {
+    this.checkoutReview()
+  },
+  methods: {
+    checkoutReview () {
+      this.basket.loading = true
+      APIGateway.basket.checkoutReview()
+        .then((basket) => {
+          this.basket = new Basket(basket)
+        })
+        .finally(() => {
+          this.basket.loading = false
+        })
+    },
+    onIncrease (basketItem) {
+      basketItem.loading = true
+      const newCount = basketItem.count + 1
+      APIGateway.basketItem.update(basketItem.id, newCount)
+        .then(() => {
+          this.checkoutReview()
+        })
+        .finally(() => {
+          basketItem.loading = false
+        })
+    },
+    onDecrease (basketItem) {
+      basketItem.loading = true
+      const newCount = basketItem.count - 1
+      APIGateway.basketItem.update(basketItem.id, newCount)
+        .then(() => {
+          this.checkoutReview()
+        })
+        .finally(() => {
+          basketItem.loading = false
+        })
+    },
+    onRemove (basketItem) {
+      basketItem.loading = true
+      APIGateway.basketItem.update(basketItem.id, 0)
+        .then(() => {
+          this.checkoutReview()
+        })
+        .finally(() => {
+          basketItem.loading = false
+        })
+    },
+    onComplete () {
+      this.$emit('complete')
+    }
+  }
+}
+</script>
+
+<style scoped lang="scss">
+.step1 {
+  display: flex;
+  flex-flow: row;
+  gap: 28px;
+  $sidebar-width: 312px;
+  .step1__cart-items {
+    width: calc( 100% - #{$sidebar-width} );
+    display: flex;
+    flex-flow: column;
+    gap: 24px;
+    .step1__cart-item {
+
+    }
+  }
+  .step1__sidebar {
+    width: $sidebar-width;
+  }
+}
+</style>

+ 89 - 0
src/components/Widgets/Store/Cart/components/stepper.vue

@@ -0,0 +1,89 @@
+<template>
+  <div class="stepper">
+    <q-stepper ref="stepper"
+               v-model="step"
+               flat
+               color="primary"
+               animated>
+      <q-step :name="1"
+              title="سبد خرید"
+              icon="shopping_cart"
+              active-icon="shopping_cart"
+              done-icon="shopping_cart"
+              :done="step > 1">
+        <step1 @complete="onCompleteStep1" />
+      </q-step>
+
+      <q-step :name="2"
+              title="زمان و نحوه ارسال"
+              icon="local_shipping"
+              done-icon="local_shipping"
+              active-icon="local_shipping"
+              :done="step > 2">
+        <step2 @complete="onCompleteStep2" />
+      </q-step>
+
+      <q-step :name="3"
+              title="پرداخت"
+              icon="account_balance_wallet"
+              done-icon="account_balance_wallet"
+              active-icon="account_balance_wallet"
+              disable>
+        This step won't show up because it is disabled.
+      </q-step>
+    </q-stepper>
+  </div>
+</template>
+
+<script>
+import Step1 from './step1/step1.vue'
+import Step2 from './step2/step2.vue'
+
+export default {
+  name: 'stepper',
+  components: { Step1, Step2 },
+  props: {
+    type: {
+      type: String,
+      default: 'product'
+    }
+  },
+  emits: ['onChangeFilters'],
+  data () {
+    return {
+      step: 1
+    }
+  },
+  methods: {
+    onCompleteStep1 () {
+      this.step = 2
+    },
+    onCompleteStep2 () {
+      this.step = 3
+    }
+  }
+}
+</script>
+
+<style scoped lang="scss">
+.stepper {
+  :deep(.q-stepper) {
+    .q-stepper__header {
+      height: 94px;
+      background: #F5F5F5;
+      border-radius: 8px;
+    }
+    .q-stepper__content {
+      .q-panel {
+        .q-stepper__step {
+          .q-stepper__step-content {
+            .q-stepper__step-inner {
+              padding: 40px 0 1px 0;
+            }
+          }
+        }
+      }
+    }
+  }
+}
+</style>

+ 18 - 1
src/components/Widgets/Store/Shop/components/packageItem.vue

@@ -34,7 +34,9 @@
         مشاهده جزییات
       </q-btn>
       <q-btn class="package-item__actions-add-to-cart"
-             color="primary">
+             :loading="addToCartLoading"
+             color="primary"
+             @click="addToCart">
         افزودن به سبد
       </q-btn>
     </div>
@@ -43,6 +45,7 @@
 
 <script>
 import { Package } from 'src/models/Package.js'
+import { APIGateway } from 'src/api/APIGateway.js'
 
 export default {
   name: 'PackageItem',
@@ -51,6 +54,20 @@ export default {
       type: Package,
       default: new Package()
     }
+  },
+  data () {
+    return {
+      addToCartLoading: false
+    }
+  },
+  methods: {
+    addToCart () {
+      this.addToCartLoading = true
+      APIGateway.basketItem.addPackage(this.packageItem.id, 1)
+        .finally(() => {
+          this.addToCartLoading = false
+        })
+    }
   }
 }
 </script>

+ 18 - 1
src/components/Widgets/Store/Shop/components/productItem.vue

@@ -42,7 +42,9 @@
         مشاهده جزییات
       </q-btn>
       <q-btn class="product-item__actions-add-to-cart"
-             color="primary">
+             :loading="addToCartLoading"
+             color="primary"
+             @click="addToCart">
         افزودن به سبد
       </q-btn>
     </div>
@@ -51,6 +53,7 @@
 
 <script>
 import { Product } from 'src/models/Product.js'
+import { APIGateway } from 'src/api/APIGateway.js'
 
 export default {
   name: 'ProductItem',
@@ -59,6 +62,20 @@ export default {
       type: Product,
       default: new Product()
     }
+  },
+  data () {
+    return {
+      addToCartLoading: false
+    }
+  },
+  methods: {
+    addToCart () {
+      this.addToCartLoading = true
+      APIGateway.basketItem.addProduct(this.product.id, 1)
+        .finally(() => {
+          this.addToCartLoading = false
+        })
+    }
   }
 }
 </script>

+ 12 - 2
src/components/Widgets/Store/StorePackage/StorePackageShow/StorePackageShow.vue

@@ -52,7 +52,9 @@
         <q-btn label="افزودن به سبد"
                outline
                color="primary"
-               class="package-show__summery-price-action" />
+               :loading="addToCartLoading"
+               class="package-show__summery-price-action"
+               @click="addToCart" />
       </div>
     </div>
     <div class="package-show__products">
@@ -91,7 +93,8 @@ export default {
   mixins: [mixinWidget],
   data () {
     return {
-      storePackage: new Package()
+      storePackage: new Package(),
+      addToCartLoading: false
     }
   },
   computed: {
@@ -104,6 +107,13 @@ export default {
     this.getPackage()
   },
   methods: {
+    addToCart () {
+      this.addToCartLoading = true
+      APIGateway.basketItem.addProduct(this.packageId, 1)
+        .finally(() => {
+          this.addToCartLoading = false
+        })
+    },
     getPackage () {
       this.storePackage.loading = true
       APIGateway.package.get(this.packageId)

+ 12 - 2
src/components/Widgets/Store/StoreProduct/StoreProductShow/StoreProductShow.vue

@@ -64,7 +64,9 @@
         <q-btn label="افزودن به سبد"
                outline
                color="primary"
-               class="product-show__summery-price-action" />
+               :loading="addToCartLoading"
+               class="product-show__summery-price-action"
+               @click="addToCart" />
       </div>
     </div>
     <div v-if="false"
@@ -104,7 +106,8 @@ export default {
   mixins: [mixinWidget],
   data () {
     return {
-      product: new Product()
+      product: new Product(),
+      addToCartLoading: false
     }
   },
   computed: {
@@ -117,6 +120,13 @@ export default {
     this.getPackage()
   },
   methods: {
+    addToCart () {
+      this.addToCartLoading = true
+      APIGateway.basketItem.addProduct(this.productId, 1)
+        .finally(() => {
+          this.addToCartLoading = false
+        })
+    },
     getPackage () {
       this.product.loading = true
       APIGateway.product.get(this.productId)

+ 74 - 46
src/models/Basket.js

@@ -1,21 +1,13 @@
 import { User } from 'src/models/User.js'
+import { Invoice } from 'src/models/Invoice.js'
+import { Discount } from 'src/models/Discount.js'
 import { Model, Collection } from 'js-abstract-model'
-import { RegistrationList } from 'src/models/Registration.js'
+import { BasketItemList } from 'src/models/BasketItem.js'
 
 class Basket extends Model {
   constructor (data) {
     super(data, [
       { key: 'id' },
-      // -----------------
-      { key: 'unit_price' },
-      { key: 'title' },
-      { key: 'thumbnail' },
-      { key: 'slug' },
-      { key: 'slug' },
-      { key: 'product_type' },
-      { key: 'description' },
-      { key: 'capacity' },
-      // -----------------
       { key: 'creator' },
       {
         key: 'creator_info',
@@ -26,51 +18,87 @@ class Basket extends Model {
         key: 'owner_info',
         relatedModel: User
       },
+      { key: 'description' },
+      { key: 'status' },
       {
-        key: 'registrations_info',
-        relatedModel: RegistrationList
+        key: 'price',
+        default: 0
       },
-      { key: 'status' },
-      { key: 'amount' },
-      { key: 'description' },
-      { key: 'creation_time' },
-      { key: 'last_modification_time' },
-      { key: 'is_active' },
-      { key: 'is_deleted' }
-    ])
-
-    this.statusEnums = [
       {
-        label: 'باز',
-        value: 'OPEN'
+        key: 'sellable_price',
+        default: 0
       },
       {
-        label: 'لغو شده',
-        value: 'CANCELED'
+        key: 'margin_amount',
+        default: 0
       },
+      { key: 'discount' },
       {
-        label: 'بسته شده',
-        value: 'CLOSED'
+        key: 'discount_info',
+        relatedModel: Discount
       },
       {
-        label: 'پرداخت شده',
-        value: 'PAID'
-      }
-    ]
-
-    this.loadStatusInfo()
-  }
-
-  loadStatusInfo () {
-    const target = this.statusEnums.find(type => type.value === this.status)
-    if (!target) {
-      this.status_info = {
-        label: null,
-        value: null
+        key: 'discount_amount',
+        default: 0
+      },
+      {
+        key: 'order_price',
+        default: 0
+      },
+      {
+        key: 'overall_discount_amount',
+        default: 0
+      },
+      {
+        key: 'overall_order_price',
+        default: 0
+      },
+      { key: 'order_time' },
+      { key: 'invoice' },
+      {
+        key: 'invoice_info',
+        relatedModel: Invoice
+      },
+      { key: 'province' },
+      { key: 'city' },
+      { key: 'address' },
+      { key: 'postal_code' },
+      {
+        key: 'delivery_cost',
+        default: 0
+      },
+      {
+        key: 'items_info',
+        relatedModel: BasketItemList
+      },
+      { key: 'creation_time' },
+      { key: 'last_modification_time' },
+      { key: 'is_active' },
+      { key: 'is_deleted' }
+    ], {
+      status: {
+        infoKey: 'status_info',
+        enumListKey: 'statusEnums',
+        enums: [
+          {
+            label: 'باز',
+            value: 'OPEN'
+          },
+          {
+            label: 'لغو شده',
+            value: 'CANCELED'
+          },
+          {
+            label: 'بسته شده',
+            value: 'CLOSED'
+          },
+          {
+            label: 'پرداخت شده',
+            value: 'PAID'
+          }
+        ]
       }
-    } else {
-      this.status_info = target
-    }
+    })
   }
 }
 

+ 40 - 0
src/models/BasketItem.js

@@ -0,0 +1,40 @@
+// import { Basket } from 'src/models/Basket.js'
+import { Package } from 'src/models/Package.js'
+import { Product } from 'src/models/Product.js'
+import { Model, Collection } from 'js-abstract-model'
+
+class BasketItem extends Model {
+  constructor (data) {
+    super(data, [
+      { key: 'id' },
+      // -----------------
+      { key: 'basket' },
+      // {
+      //   key: 'basket_info',
+      //   relatedModel: Basket
+      // },
+      { key: 'package' },
+      {
+        key: 'package_info',
+        relatedModel: Package
+      },
+      { key: 'product' },
+      {
+        key: 'product_info',
+        relatedModel: Product
+      },
+      {
+        key: 'count',
+        default: 0
+      }
+    ])
+  }
+}
+
+class BasketItemList extends Collection {
+  model() {
+    return BasketItem
+  }
+}
+
+export { BasketItem, BasketItemList }

+ 23 - 0
src/models/Discount.js

@@ -0,0 +1,23 @@
+import { Model, Collection } from 'js-abstract-model'
+
+class Discount extends Model {
+  constructor (data) {
+    super(data, [
+      { key: 'id' },
+      { key: 'code' },
+      {
+        key: 'percent',
+        default: 0
+      },
+      { key: 'expire_time' }
+    ])
+  }
+}
+
+class DiscountList extends Collection {
+  model() {
+    return Discount
+  }
+}
+
+export { Discount, DiscountList }

+ 8 - 0
src/router/routes/Public.js

@@ -104,6 +104,14 @@ export default {
       },
       component: () => import('src/pages/BasicDynamicPage.vue')
     },
+    {
+      name: 'Public.Cart',
+      path: 'cart',
+      meta: {
+        hasDynamicSetting: true
+      },
+      component: () => import('src/pages/BasicDynamicPage.vue')
+    },
     {
       name: 'Public.Rules',
       path: 'rules',