ali 2 роки тому
батько
коміт
4abb9283e5

+ 3 - 0
src/components/FormBuilderCustumComponents/Hashtags/HashtagSelector.vue

@@ -13,6 +13,9 @@
               :options="filteredHashtags"
               @update:model-value="onChangeSelected"
               @filter="filterFn">
+      <template #prepend>
+        <q-icon name="search" />
+      </template>
       <template v-slot:no-option>
         <span v-if="fetchLoading">
           کمی صبر کنید...

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

@@ -123,6 +123,11 @@
       </q-btn>
 
       <div class="left-side">
+        <q-btn flat
+               :to="{ name: 'Public.PostTopic.List' }"
+               class="btn-cart">
+          <q-icon name="search" />
+        </q-btn>
         <q-btn v-if="isUserLogin"
                flat
                :to="{ name: 'Public.Cart' }"

+ 193 - 0
src/components/Widgets/Content/ContentTopicList/ContentTopicList.vue

@@ -0,0 +1,193 @@
+<template>
+  <div class="ContentTopicList"
+       :style="localOptions.style">
+    <template v-if="mounted">
+      <breadcrumbs class="q-mb-lg" />
+      <div class="ContentTopicList__header">
+        <q-banner>
+          جستجوی پیشرفته محتوایی
+        </q-banner>
+        <q-btn color="primary"
+               @click="downloadExcel">
+          <q-icon name="file_download" />
+          دریافت
+        </q-btn>
+      </div>
+      <div class="ContentTopicList__top-filter">
+        <form-builder v-model:value="topFilterInputs"
+                      @update:value="onTopFilterChange" />
+      </div>
+      <div class="ContentTopicList__main">
+        <div class="ContentTopicList__sidebar">
+          <sidebar @onChangeFilters="onChangeSidebarFilters" />
+        </div>
+        <div class="ContentTopicList__result">
+          <entity-index v-if="mounted"
+                        ref="entityIndex"
+                        v-model:value="inputs"
+                        :api="api"
+                        :table="table"
+                        :table-keys="tableKeys"
+                        :table-grid-size="true"
+                        :default-layout="false"
+                        :create-route-name="createRouteName"
+                        :show-search-button="false"
+                        :show-expand-button="false"
+                        :show-reload-button="false">
+            <template #entity-index-table-item-cell="{inputData}">
+              <topic-item class="topic-item"
+                          :topic="inputData.props.row" />
+            </template>
+          </entity-index>
+        </div>
+      </div>
+    </template>
+  </div>
+</template>
+
+<script>
+import { shallowRef } from 'vue'
+import { EntityIndex } from 'quasar-crud'
+import { Ticket } from 'src/models/Ticket.js'
+import Sidebar from './components/sidebar.vue'
+import TopicItem from './components/TopicItem.vue'
+import { mixinWidget } from 'src/mixin/Mixins.js'
+import { APIGateway } from 'src/api/APIGateway.js'
+import { FormBuilder, FormBuilderAssist } from 'quasar-form-builder'
+import Breadcrumbs from 'src/components/Widgets/Breadcrumbs/Breadcrumbs.vue'
+import HashtagSelector from 'src/components/FormBuilderCustumComponents/Hashtags/HashtagSelector.vue'
+
+const HashtagSelectorComp = shallowRef(HashtagSelector)
+
+export default {
+  name: 'ContentList',
+  components: { EntityIndex, Breadcrumbs, Sidebar, FormBuilder, TopicItem },
+  mixins: [mixinWidget],
+  data () {
+    return {
+      breadcrumbs: {
+        visible: true,
+        loading: false,
+        path: [
+          {
+            label: 'خانه',
+            to: { name: 'Public.Home' }
+          },
+          {
+            label: 'جستجوی پیشرفته محتوایی',
+            to: { name: 'Public.PostTopic.List' }
+          }
+        ]
+      },
+      api: APIGateway.contentTopic.APIAdresses.base,
+      tableKeys: {
+        data: 'results',
+        total: 'count',
+        currentPage: 'current',
+        perPage: 'per_page',
+        pageKey: 'page'
+      },
+      inputs: [
+        { type: 'hidden', name: 'category__in', value: null },
+        { type: 'hidden', name: 'hashtags__id__in', value: null }
+      ],
+      topFilterInputs: [
+        { type: HashtagSelectorComp, name: 'hashtags', responseKey: 'hashtags_info', col: 'col-12' }
+      ],
+      topFilter: {
+        hashtags: null
+      },
+      table: {
+        columns: [
+          {
+            name: 'title',
+            required: true,
+            label: 'موضوع پیام',
+            align: 'left',
+            field: row => row.title
+          },
+          {
+            name: 'sender',
+            required: true,
+            label: 'فرستنده',
+            align: 'left',
+            field: row => row.creator_info.firstname + ' ' + row.creator_info.lastname
+          },
+          {
+            name: 'status',
+            required: true,
+            label: 'وضعیت',
+            align: 'left',
+            field: row => (new Ticket(row)).status_info.label
+          },
+          {
+            name: 'action',
+            required: true,
+            label: 'جزییات',
+            align: 'left',
+            field: row => ''
+          }
+        ]
+      },
+      mounted: false,
+      createRouteName: ''
+    }
+  },
+  mounted() {
+    this.$store.commit('AppLayout/updateBreadcrumbs', this.breadcrumbs)
+    this.mounted = true
+  },
+  methods: {
+    onChangeSidebarFilters (filters) {
+      this.sidebarFilters = filters
+      this.search()
+    },
+    onTopFilterChange () {
+      this.topFilter = FormBuilderAssist.getFormData(this.topFilterInputs)
+      this.search()
+    },
+    search () {
+      this.updateFilters()
+      if (!this.$refs.entityIndex) {
+        return
+      }
+
+      this.$refs.entityIndex.search()
+    },
+    updateFilters () {
+      FormBuilderAssist.setAttributeByName(this.inputs, 'category__in', 'value', this.sidebarFilters.category__in)
+      FormBuilderAssist.setAttributeByName(this.inputs, 'hashtags__id__in', 'value', this.topFilter.hashtags)
+    },
+    downloadExcel () {
+    }
+  }
+}
+</script>
+
+<style scoped lang="scss">
+.ContentTopicList {
+  .ContentTopicList__header {
+    display: flex;
+    justify-content: space-between;
+    margin-bottom: 24px;
+  }
+  .ContentTopicList__top-filter {
+    margin-bottom: 24px;
+  }
+  .ContentTopicList__main {
+    display: flex;
+    gap: 24px;
+    justify-content: flex-start;
+    $sidebar-width: 312px;
+    .ContentTopicList__sidebar {
+      width: $sidebar-width;
+    }
+    .ContentTopicList__result {
+      width: calc( 100% - #{$sidebar-width} );
+      .topic-item {
+        width: 100%;
+      }
+    }
+  }
+}
+</style>

+ 21 - 0
src/components/Widgets/Content/ContentTopicList/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>

+ 156 - 0
src/components/Widgets/Content/ContentTopicList/components/TopicItem.vue

@@ -0,0 +1,156 @@
+<template>
+  <div class="topic-item-wrapper">
+    <div class="topic-item-side">
+      <div class="topic-item-side__line" />
+      <div class="topic-item-side__dot" />
+      <div class="topic-item-side__arrow" />
+    </div>
+    <div class="topic-item">
+      <div class="topic-item__title">
+        {{ topic.title }}
+      </div>
+      <div class="topic-item__text"
+           v-html="topic.text" />
+      <div class="topic-item__content">
+        <div class="topic-item__content-label">
+          <q-icon name="description"
+                  size="20px"
+                  color="primary" />
+          محتوا:
+        </div>
+        <div class="topic-item__content-title">
+          <q-btn flat
+                 color="primary"
+                 :to="{name: 'Public.Content.Show', params: { id: topic.content_info.id }}">
+            {{ topic.content_info.title }}
+          </q-btn>
+        </div>
+      </div>
+      <div class="topic-item__hashtags">
+        <div class="topic-item__hashtags-label">
+          <q-icon name="layers"
+                  size="20px"
+                  color="primary" />
+          برچسب ها:
+        </div>
+        <div class="topic-item__hashtags-items">
+          <div v-for="(hashtag, hashtagIndex) in topic.hashtags_info"
+               :key="hashtagIndex"
+               class="topic-item__hashtags-item">
+            {{ hashtag.title }}
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+import { ContentTopic } from 'src/models/ContentTopic.js'
+
+export default {
+  name: 'TopicItem',
+  props: {
+    topic: {
+      type: Object,
+      default: new ContentTopic()
+    }
+  },
+  data () {
+    return {
+      addToCartLoading: false
+    }
+  }
+}
+</script>
+
+<style scoped lang="scss">
+.topic-item-wrapper {
+  display: flex;
+  flex-flow: row;
+  justify-content: flex-start;
+  $side-width: 32px;
+  .topic-item-side {
+    width: $side-width;
+    position: relative;
+    .topic-item-side__line {
+      position: absolute;
+      top: 0;
+      left: 0;
+      width: 4px;
+      height: 100%;
+      background: $primary;
+    }
+    .topic-item-side__dot {
+      width: 16px;
+      height: 16px;
+      background: white;
+      position: absolute;
+      top: 32px;
+      left: 0;
+      transform: translateX(calc( -50% + 2px ));
+      &:after {
+        content: ' ';
+        position: absolute;
+        left: 50%;
+        top: 50%;
+        width: 8px;
+        height: 8px;
+        border-radius: 100%;
+        background: $primary;
+        transform: translateX(-50%) translateY(-50%);
+      }
+    }
+    .topic-item-side__arrow {
+      width: 16px;
+      height: 16px;
+      background: white;
+      position: absolute;
+      top: 24px;
+      right: 0;
+      border-left: solid 1px $primary;
+      border-bottom: solid 1px $primary;
+      transform: rotate( 45deg ) translateX(12px);
+    }
+  }
+  .topic-item {
+    width: calc( 100% - #{$side-width} );
+    border: 1px solid $primary;
+    border-radius: 8px;
+    padding: 16px;
+    margin: 8px 0;
+    .topic-item__title {
+      color: $primary;
+      margin-bottom: 16px;
+    }
+    .topic-item__text {
+      margin-bottom: 32px;
+    }
+    .topic-item__content {
+      display: flex;
+      justify-content: flex-start;
+      align-items: center;
+    }
+    .topic-item__hashtags {
+      display: flex;
+      justify-content: flex-start;
+      align-items: center;
+      .topic-item__hashtags-label {
+        margin-right: 8px;
+      }
+      .topic-item__hashtags-items {
+        display: flex;
+        flex-wrap: wrap;
+        justify-content: flex-start;
+        align-items: center;
+        gap: 8px;
+        .topic-item__hashtags-item {
+          padding: 6px 10px;
+          border: solid 1px #b9b9b9;
+          border-radius: 50px;
+        }
+      }
+    }
+  }
+}
+</style>

+ 194 - 0
src/components/Widgets/Content/ContentTopicList/components/packageItem.vue

@@ -0,0 +1,194 @@
+<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 { Basket } from 'src/models/Basket.js'
+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
+          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">
+.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>

+ 205 - 0
src/components/Widgets/Content/ContentTopicList/components/productItem.vue

@@ -0,0 +1,205 @@
+<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 { Basket } from 'src/models/Basket.js'
+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
+          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;
+    .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: 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;
+      .q-img {
+        width: 90px;
+      }
+    }
+    .product-item__title {
+      margin-bottom: 44px;
+    }
+    .product-item__price {
+      margin-bottom: 27px;
+    }
+    .product-item__actions {
+      display: flex;
+      flex-flow: column;
+    }
+  }
+}
+</style>

+ 213 - 0
src/components/Widgets/Content/ContentTopicList/components/sidebar.vue

@@ -0,0 +1,213 @@
+<template>
+  <div class="sidebar">
+    <div class="sidebar__header">
+      <div class="sidebar__header-title">
+        فیلترها
+      </div>
+      <q-btn flat
+             class="sidebar__header-remove-filters"
+             @click="clearFilters">
+        <q-icon name="delete_outline" />
+        حذف فیلترها
+      </q-btn>
+    </div>
+    <div class="sidebar__selected-filters">
+      <div v-for="(tag, tagIndex) in selectedFilters"
+           :key="tagIndex"
+           class="sidebar__selected-filter">
+        <div class="sidebar__selected-filter-title">
+          {{ tag.label }}
+        </div>
+        <q-btn class="sidebar__selected-filter-btn-remove"
+               flat
+               icon="close"
+               @click="removeFilterTag(tag)" />
+      </div>
+    </div>
+    <q-separator />
+    <q-linear-progress v-if="contentTopicCategories.loading"
+                       indeterminate />
+    <q-tree v-if="!contentTopicCategories.loading"
+            v-model:ticked="filters.categories"
+            class="col-12 col-sm-6"
+            :nodes="contentTopicCategories.list"
+            children-key="children"
+            label-key="title"
+            node-key="id"
+            tick-strategy="leaf"
+            @update:ticked="updateFilter" />
+  </div>
+</template>
+
+<script>
+import { APIGateway } from 'src/api/APIGateway'
+import { ContentTopicCategoryList } from 'src/models/ContentTopicCategory.js'
+
+export default {
+  name: 'sidebar',
+  props: {
+    type: {
+      type: String,
+      default: 'product'
+    }
+  },
+  emits: ['onChangeFilters'],
+  data () {
+    return {
+      filters: {
+        categories: [] // content_category
+      },
+      contentTopicCategories: new ContentTopicCategoryList(),
+      mounted: false
+    }
+  },
+  computed: {
+    selectedFilters () {
+      const tags = []
+
+      if (this.filters.categories.length > 0) {
+        this.filters.categories.map(categoryId => this.getCategory(categoryId)).filter(item => item).forEach(item => {
+          tags.push({
+            label: item.title,
+            filterKey: 'contentTopicCategories',
+            value: item
+          })
+        })
+      }
+
+      return tags
+    }
+  },
+  mounted() {
+    this.getStoreCategories()
+  },
+  methods: {
+    updateFilter () {
+      this.$emit('onChangeFilters', {
+        category__in: this.filters.categories
+      })
+    },
+    removeCategoryById(categories, idKey, categoryValue) {
+      const targetIndex = categories.findIndex(item => item[idKey] === categoryValue)
+      categories.splice(targetIndex, 1)
+    },
+    getCategoryById(categories, idKey, categoryValue) {
+      for (const category of categories) {
+        if (category[idKey] === categoryValue) {
+          return category
+        }
+        if (category.children && category.children.length > 0) {
+          const result = this.getCategoryById(category.children, idKey, categoryValue)
+          if (result) {
+            return result
+          }
+        }
+      }
+      return null // Return null if category with given id is not found
+    },
+    getCategory (categoryId) {
+      return this.getCategoryById(this.contentTopicCategories.list, 'id', categoryId)
+    },
+    removeFilterTag (filterTagItem) {
+      if (filterTagItem.filterKey === 'contentTopicCategories') {
+        this.removeCategoryById(this.filters.categories, 'id', filterTagItem.value.id)
+      }
+      this.updateFilter()
+    },
+    clearFilters () {
+      this.filters.categories = []
+      this.updateFilter()
+    },
+    getStoreCategories () {
+      this.contentTopicCategories.loading = true
+      APIGateway.contentTopicCategory.index({ parent__isnull: 'true', per_page: 99999 })
+        .then(({ list }) => {
+          this.contentTopicCategories = list
+        })
+        .finally(() => {
+          this.contentTopicCategories.loading = false
+        })
+    }
+  }
+}
+</script>
+
+<style scoped lang="scss">
+.sidebar {
+  background: #F6F6F6;
+  border-radius: 20px;
+  padding: 24px;
+  .sidebar__header {
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+    margin-bottom: 15px;
+    .sidebar__header-title {
+      font: normal normal medium 16px/21px AzarMehr;
+      letter-spacing: 0;
+    }
+    .sidebar__header-remove-filters {
+      color: #FF3D3D;
+    }
+  }
+  .sidebar__selected-filters {
+    display: flex;
+    gap: 8px;
+    flex-wrap: wrap;
+    justify-content: flex-start;
+    .sidebar__selected-filter {
+      border: 1px solid #ABABAB;
+      border-radius: 20px;
+      padding: 8px;
+      display: flex;
+      justify-content: space-between;
+      gap: 8px;
+      align-items: center;
+      background: #FFFFFF;
+      .sidebar__selected-filter-title {
+        font-size: 14px;
+        color: #212121;
+      }
+      :deep(.sidebar__selected-filter-btn-remove) {
+        @mixin set-btn-min-max () {
+          $btn-size: 20px;
+          max-height: $btn-size;
+          max-width: $btn-size;
+          min-width: $btn-size;
+          min-height: $btn-size;
+        }
+        padding: 1px;
+        @include set-btn-min-max;
+        .q-focus-helper {
+          @include set-btn-min-max;
+        }
+        .q-btn__content {
+          @include set-btn-min-max;
+          .q-icon {
+            font-size: 18px;
+          }
+        }
+      }
+    }
+  }
+  .q-separator {
+    margin: 18px 0;
+  }
+  .sidebar__price-filter {
+    .sidebar__price-label {
+      display: flex;
+      justify-content: space-between;
+      align-items: center;
+      font-size: 14px;
+      color: #212121;
+      .sidebar__price-label-min {}
+      .sidebar__price-label-max {}
+    }
+  }
+  .sidebar__exist-filter {
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+  }
+}
+</style>

+ 1 - 1
src/components/Widgets/Post/PostList/PostList.vue

@@ -38,7 +38,7 @@ export default {
   name: 'PostList',
   components: { PostItem, EntityIndex, Breadcrumbs },
   mixins: [mixinWidget],
-  data: () => {
+  data () {
     return {
       api: APIGateway.post.APIAdresses.base,
       tableKeys: {

+ 6 - 2
src/css/Theme/Controls/select.scss

@@ -1,6 +1,10 @@
 .q-select {
   .q-field__inner {
     .q-field__control {
+      .q-field__prepend {
+        padding-left: 12px;
+        height: 100%;
+      }
       .q-field__control-container {
         padding-top: 0;
         .q-field__native {
@@ -9,8 +13,8 @@
         }
       }
       .q-field__append {
-        min-height: 48px;
-        height: 48px;
+        padding-right: 12px;
+        height: 100%;
       }
     }
   }

+ 15 - 0
src/pages/Public/Content/Show.vue

@@ -0,0 +1,15 @@
+<template>
+  <q-page-builder v-model:sections="currenSections"
+                  v-model:options="pageConfig"
+                  :editable="pageBuilderEditable"
+                  :loading="pageBuilderLoading" />
+</template>
+
+<script>
+import { mixinPageOptions, mixinPrefetchServerData, mixinSEO } from 'src/mixin/Mixins.js'
+
+export default {
+  name: 'AdminPanel.Package.Show',
+  mixins: [mixinPrefetchServerData, mixinPageOptions, mixinSEO]
+}
+</script>

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

@@ -28,6 +28,29 @@ export default {
         { path: ':id', name: 'Public.Post.Show', component: () => import('src/pages/Public/Post/Show.vue') }
       ]
     },
+    {
+      path: 'content',
+      name: 'Public.Content',
+      component: () => import('layouts/bareLayout.vue'),
+      meta: {
+        hasDynamicSetting: true
+      },
+      children: [
+        { path: ':id', name: 'Public.Content.Show', component: () => import('src/pages/Public/Content/Show.vue') }
+      ]
+    },
+    {
+      path: 'post-topic',
+      name: 'Public.PostTopic',
+      component: () => import('layouts/bareLayout.vue'),
+      meta: {
+        hasDynamicSetting: true
+      },
+      children: [
+        { path: '', name: 'Public.PostTopic.List', component: () => import('src/pages/BasicDynamicPage.vue') },
+        { path: ':id', name: 'Public.PostTopic.Show', component: () => import('src/pages/BasicDynamicPage.vue') }
+      ]
+    },
     { path: 'post_category/:category_id', name: 'Public.PostCategory.Show', component: () => import('src/pages/Public/Post/ChildCategoryPostList.vue') },
     {
       name: 'Public.AllClassrooms',