ali %!s(int64=2) %!d(string=hai) anos
pai
achega
fb5c7f2f67

+ 28 - 0
src/components/Widgets/Store/StoreProduct/ProductCarousel/OptionPanel.vue

@@ -0,0 +1,28 @@
+<template>
+  <option-panel-tabs v-model:options="localOptions">
+    <template #main-tab>
+      <q-option-group v-model="localOptions.classroomType"
+                      :options="classroomTypeOptions" />
+    </template>
+  </option-panel-tabs>
+</template>
+
+<script>
+import { defineComponent } from 'vue'
+import { UnitCategory } from 'src/models/UnitCategory.js'
+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],
+  data () {
+    return {
+      classroomTypeOptions: (new UnitCategory()).typeEnums
+    }
+  }
+})
+</script>

+ 143 - 0
src/components/Widgets/Store/StoreProduct/ProductCarousel/ProductCarousel.vue

@@ -0,0 +1,143 @@
+<template>
+  <div class="productCarousel">
+    <div class="productCarousel-title">
+      <div>
+        <q-banner class="banner">
+          فروشگاه
+        </q-banner>
+      </div>
+      <div>
+        <q-btn flat
+               :to="{name: 'Public.Shop'}">
+          مشاهده همه
+          <svg xmlns="http://www.w3.org/2000/svg"
+               width="18.387"
+               height="11.502"
+               viewBox="0 0 18.387 11.502"
+               class="q-ml-sm">
+            <path id="Combined_Shape"
+                  data-name="Combined Shape"
+                  d="M-9.338,11.408a.748.748,0,0,0,.388-.656V6.5h8.2A.751.751,0,0,0,0,5.75.75.75,0,0,0-.75,5h-8.2V.75A.751.751,0,0,0-9.338.093.769.769,0,0,0-9.7,0a.735.735,0,0,0-.4.115l-7.938,5a.747.747,0,0,0-.35.635.742.742,0,0,0,.35.634l7.938,5a.751.751,0,0,0,.4.116A.746.746,0,0,0-9.338,11.408ZM-10.45,9.392l-5.78-3.641,5.78-3.642Z"
+                  transform="translate(18.387)"
+                  fill="#eac38a" />
+          </svg>
+        </q-btn>
+      </div>
+    </div>
+    <div class="carousel-section">
+      <div v-if="!products.loading && products.list.length > 0"
+           ref="slider"
+           class="splide"
+           role="group"
+           aria-label="Splide Basic HTML Example">
+        <div class="splide__track">
+          <ul class="splide__list">
+            <li v-for="(product, productIndex) in products.list"
+                :key="productIndex"
+                class="splide__slide">
+              <product-item :product="product" />
+            </li>
+          </ul>
+        </div>
+      </div>
+      <div v-else-if="!products.loading && products.list.length === 0">
+        رویدادی یافت نشد.
+      </div>
+      <div v-else-if="products.loading">
+        کمی صبر کنید ...
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+import Splide from '@splidejs/splide'
+import ProductItem from './productItem.vue'
+import { APIGateway } from 'src/api/APIGateway.js'
+import { ProductList } from 'src/models/Product.js'
+import '@splidejs/splide/dist/css/splide-core.min.css'
+import { mixinPrefetchServerData } from 'src/mixin/Mixins.js'
+
+export default {
+  name: 'ProductCarousel',
+  components: {
+    ProductItem
+  },
+  mixins: [mixinPrefetchServerData],
+  data () {
+    return {
+      loading: false,
+      slider: null,
+      products: new ProductList(),
+      breakpoints: {
+        1920: {
+          perPage: 4
+        },
+        900: {
+          perPage: 2
+        },
+        500: {
+          perPage: 1
+        }
+      },
+      maximizedToggle: true,
+      dialog: false
+    }
+  },
+  mounted () {
+    this.loadSlider()
+  },
+  methods: {
+    loadSlider () {
+      this.$nextTick(() => {
+        if (!this.$refs.slider) {
+          return
+        }
+        new Splide(this.$refs.slider, {
+          direction: 'rtl',
+          paginationDirection: 'rtl',
+          // type: 'loop',
+          focus: 0,
+          gap: 24,
+          snap: true,
+          // focus: 'center',
+          breakpoints: this.breakpoints
+        }).mount()
+      })
+    },
+    prefetchServerDataPromise () {
+      return this.getProducts()
+    },
+    prefetchServerDataPromiseThen (productList) {
+      this.products = new ProductList(productList.list)
+      this.products.loading = false
+      this.loadSlider()
+    },
+    prefetchServerDataPromiseCatch () {
+      this.products.loading = false
+    },
+
+    getProducts () {
+      this.products.loading = true
+      // return APIGateway.product.index()
+      return APIGateway.product.index({
+        per_page: 20
+      })
+    }
+  }
+}
+</script>
+
+<style scoped lang="scss">
+.productCarousel {
+  padding: 60px 0;
+  .productCarousel-title {
+    display: flex;
+    flex-flow: row;
+    justify-content: space-between;
+    padding-bottom: 33px;
+  }
+  .carousel-section {
+  }
+}
+</style>

+ 277 - 0
src/components/Widgets/Store/StoreProduct/ProductCarousel/productItem.vue

@@ -0,0 +1,277 @@
+<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 v-if="product.physical_type"
+           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">
+      <img :src="product.thumbnail">
+    </div>
+    <div class="product-item__title">
+      {{ product.title }}
+    </div>
+    <div class="product-item__price">
+      <div v-if="product.unit_price != product.sellable_price"
+           class="product-item__price-base">
+        {{ parseInt(product.unit_price).toLocaleString('fa') }}
+        تومان
+      </div>
+      <div class="product-item__price-final">
+        {{ product.sellable_price.toLocaleString('fa') }}
+        تومان
+      </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 v-if="!basketItem"
+             class="product-item__actions-add-to-cart"
+             :loading="addToCartLoading"
+             color="primary"
+             @click="addToCart">
+        افزودن به سبد
+      </q-btn>
+      <cart-count-action v-else
+                         :basket-item="basketItem"
+                         @increase="onIncrease"
+                         @decrease="onDecrease"
+                         @remove="onRemove" />
+    </div>
+  </div>
+</template>
+
+<script>
+import { Basket } from 'src/models/Basket.js'
+import { Product } from 'src/models/Product.js'
+import { APIGateway } from 'src/api/APIGateway.js'
+import CartCountAction from 'src/components/cart/cartCountAction/cartCountAction.vue'
+
+export default {
+  name: 'ProductItem',
+  components: { CartCountAction },
+  props: {
+    product: {
+      type: Product,
+      default: new Product()
+    }
+  },
+  data () {
+    return {
+      addToCartLoading: false
+    }
+  },
+  computed: {
+    basket () {
+      return this.$store.getters['Shop/basket']
+    },
+    basketItem () {
+      const target = this.basket.items_info.list.find(basketItem => basketItem.product === this.product.id)
+      if (!target) {
+        return null
+      }
+
+      return target
+    }
+  },
+  methods: {
+    onIncrease () {
+      this.addToCartLoading = true
+      APIGateway.basketItem.incrementProduct(this.basketItem.product)
+        .finally(() => {
+          this.checkoutReview()
+          this.addToCartLoading = false
+        })
+    },
+    onDecrease () {
+      this.addToCartLoading = true
+      APIGateway.basketItem.decrementProduct(this.basketItem.product)
+        .finally(() => {
+          this.checkoutReview()
+          this.addToCartLoading = false
+        })
+    },
+    onRemove () {
+      this.addToCartLoading = true
+      APIGateway.basketItem.remove(this.basketItem.id)
+        .finally(() => {
+          this.checkoutReview()
+          this.addToCartLoading = false
+        })
+    },
+    addToCart () {
+      this.addToCartLoading = true
+      APIGateway.basketItem.addProduct(this.product.id, 1)
+        .finally(() => {
+          this.addToCartLoading = false
+          this.checkoutReview()
+        })
+    },
+    checkoutReview () {
+      this.addToCartLoading = true
+      APIGateway.basket.checkoutReview()
+        .then((basket) => {
+          this.$store.commit('Shop/updateBasket', new Basket(basket))
+        })
+        .finally(() => {
+          this.addToCartLoading = false
+        })
+    }
+  }
+}
+</script>
+
+<style scoped lang="scss">
+.product-item {
+  $transition-time: 0s;
+  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;
+    display: flex;
+    justify-content: center;
+    align-items: center;
+    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: 18px;
+      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 auto 4px auto;
+      img {
+        width: 90px;
+      }
+    }
+    .product-item__title {
+      margin-bottom: 44px;
+    }
+    .product-item__price {
+      margin-bottom: 27px;
+    }
+    .product-item__actions {
+      display: flex;
+      flex-flow: column;
+    }
+  }
+  @media screen and (max-width: 1024px) {
+    & {
+      .product-item__badges {
+        z-index: 2;
+        .product-item__badge {
+          .product-item__badge-title {
+            display: block;
+          }
+        }
+      }
+      :deep(.product-item__thumbnail) {
+        z-index: 1;
+      }
+      .product-item__title {
+        margin-bottom: 44px;
+      }
+      .product-item__price {
+        margin-bottom: 27px;
+      }
+      .product-item__actions {
+        display: flex;
+        flex-flow: column;
+      }
+    }
+  }
+}
+</style>