ali vor 2 Jahren
Ursprung
Commit
be2df23378
22 geänderte Dateien mit 1240 neuen und 236 gelöschten Zeilen
  1. 0 1
      src/components/FormBuilderCustumComponents/QuestionsCreateAndAppend/CreateQuestionForm.vue
  2. 1 1
      src/components/PostItem.vue
  3. 112 0
      src/components/Widgets/Content/AdminContentCreate-old/AdminContentCreate.vue
  4. 21 0
      src/components/Widgets/Content/AdminContentCreate-old/OptionPanel.vue
  5. 109 50
      src/components/Widgets/Content/AdminContentCreate/AdminContentCreate.vue
  6. 119 0
      src/components/Widgets/Content/AdminContentList-old/AdminContentList.vue
  7. 21 0
      src/components/Widgets/Content/AdminContentList-old/OptionPanel.vue
  8. 70 41
      src/components/Widgets/Content/AdminContentList/AdminContentList.vue
  9. 162 0
      src/components/Widgets/Content/AdminContentShow-old/AdminContentShow.vue
  10. 21 0
      src/components/Widgets/Content/AdminContentShow-old/OptionPanel.vue
  11. 93 75
      src/components/Widgets/Content/AdminContentShow/AdminContentShow.vue
  12. 116 53
      src/components/Widgets/Content/ContentList/ContentList.vue
  13. 67 2
      src/components/Widgets/Content/ContentList/OptionPanel.vue
  14. 71 0
      src/components/Widgets/Content/ContentMixin.js
  15. 16 7
      src/components/Widgets/Content/ContentShow/ContentShow.vue
  16. 10 2
      src/components/Widgets/Content/ContentShow/OptionPanel.vue
  17. 61 0
      src/components/Widgets/Content/LastContents/LastContents.vue
  18. 35 0
      src/components/Widgets/Content/LastContents/OptionPanel.vue
  19. 98 0
      src/components/Widgets/Content/LastContents/PostItem.vue
  20. 1 1
      src/components/Widgets/Post/PostList/PostList.vue
  21. 1 1
      src/components/Widgets/Post/PostShow/PostShow.vue
  22. 35 2
      src/router/routes/Public.js

+ 0 - 1
src/components/FormBuilderCustumComponents/QuestionsCreateAndAppend/CreateQuestionForm.vue

@@ -81,7 +81,6 @@ export default {
   watch: {
     questionType: {
       handler (newValue) {
-        console.log(newValue)
         FormBuilderAssist.setAttributeByName(this.inputs, 'type', 'value', newValue)
       },
       immediate: true

+ 1 - 1
src/components/PostItem.vue

@@ -13,7 +13,7 @@
         </router-link>
       </div>
       <div class="child-category ellipsis-2-lines">
-        <router-link :to="{name: 'Public.PostCategory.Show', params: {category_id: post.category_info?.id}}">
+        <router-link :to="{name: 'Public.ContentCategory.Show', params: {category_id: post.category_info?.id}}">
           <q-icon name="account_tree" />
           دسته:
           {{post.category_info?.title}}

+ 112 - 0
src/components/Widgets/Content/AdminContentCreate-old/AdminContentCreate.vue

@@ -0,0 +1,112 @@
+<template>
+  <div class="AdminContentCreate"
+       :style="localOptions.style">
+    <div class="flex justify-end">
+      <q-btn flat
+             color="grey"
+             @click="$router.go(-1)">
+        بازگشت
+        >
+      </q-btn>
+    </div>
+    <entity-create v-if="mounted"
+                   ref="entityCreate"
+                   v-model:value="inputs"
+                   title="ایجاد محتوا"
+                   :api="api"
+                   :entity-id-key="entityIdKey"
+                   :entity-param-key="entityParamKey"
+                   :show-route-name="showRouteName"
+                   :show-close-button="false"
+                   :show-edit-button="false"
+                   :show-expand-button="false"
+                   :show-save-button="false"
+                   :show-reload-button="false" />
+  </div>
+</template>
+
+<script>
+import { shallowRef } from 'vue'
+import { EntityCreate } from 'quasar-crud'
+import { mixinWidget } from 'src/mixin/Mixins.js'
+import { APIGateway } from 'src/api/APIGateway.js'
+import BtnControl from 'src/components/Control/btn.vue'
+import ContentMedias from 'src/components/FormBuilderCustumComponents/ContentMedias/ContentMedias.vue'
+import ContentCategorySelector from 'src/components/FormBuilderCustumComponents/ContentCategorySelector.vue'
+
+const BtnControlComp = shallowRef(BtnControl)
+const ContentMediasComp = shallowRef(ContentMedias)
+const ContentCategorySelectorComp = shallowRef(ContentCategorySelector)
+
+export default {
+  name: 'AdminContentCreate',
+  components: { EntityCreate },
+  mixins: [mixinWidget],
+  data: () => {
+    return {
+      mounted: false,
+      entityLoading: false,
+      api: APIGateway.content.APIAdresses.base,
+      entityIdKey: 'id',
+      entityParamKey: 'id',
+      showRouteName: 'Admin.Content.Show',
+      inputs: [
+        { type: 'input', name: 'title', responseKey: 'title', label: 'عنوان', placeholder: ' ', col: 'col-md-6 col-12' },
+        { type: 'file', name: 'thumbnail', responseKey: 'thumbnail', label: 'عکس', placeholder: ' ', col: 'col-md-6 col-12' },
+        { type: ContentCategorySelectorComp, name: 'category', responseKey: 'category', col: 'col-12' },
+        { type: 'inputEditor', name: 'description', responseKey: 'description', label: 'توضیحات', col: 'col-12' },
+        { type: ContentMediasComp, name: 'medias', responseKey: 'medias_info', col: 'col-12' },
+        { type: BtnControlComp, name: 'btn', responseKey: 'btn', label: 'ایجاد محتوا', placeholder: ' ', atClick: () => {}, col: 'col-md-6' }
+      ]
+    }
+  },
+  mounted() {
+    this.setActionBtn()
+    this.mounted = true
+  },
+  methods: {
+    setActionBtn () {
+      this.inputs.forEach((item, index) => {
+        if (item.name === 'btn') {
+          this.inputs[index].atClick = this.onSubmit
+        }
+      })
+    },
+    onSubmit () {
+      this.create()
+    },
+    create() {
+      this.entityLoading = true
+      this.$refs.entityCreate.createEntity()
+        .then(() => {
+          this.entityLoading = false
+        })
+        .catch(() => {
+          this.entityLoading = false
+        })
+    }
+  }
+}
+</script>
+
+<style scoped lang="scss">
+.AdminContentCreate {
+  .title {
+    font-style: normal;
+    font-weight: 700;
+    font-size: 24px;
+    line-height: 140%;
+    color: #424242;
+    margin-bottom: 27px;
+    position: relative;
+    .back-action {
+      position: absolute;
+      right: 0;
+      top: 0;
+    }
+  }
+  :deep(.form) {
+    padding: 24px;
+  }
+}
+</style>

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

+ 109 - 50
src/components/Widgets/Content/AdminContentCreate/AdminContentCreate.vue

@@ -1,27 +1,46 @@
 <template>
-  <div class="AdminContentCreate"
+  <div class="AdminPostCreate"
        :style="localOptions.style">
-    <div class="flex justify-end">
-      <q-btn flat
-             color="grey"
-             @click="$router.go(-1)">
-        بازگشت
-        >
-      </q-btn>
+    <div class="title">
+      نوشته جدید
+      <div class="back-action">
+        <q-btn flat
+               :to="{name: 'AdminPanel.Post.List'}"
+               color="grey">
+          بازگشت
+          >
+        </q-btn>
+      </div>
     </div>
-    <entity-create v-if="mounted"
-                   ref="entityCreate"
-                   v-model:value="inputs"
-                   title="ایجاد محتوا"
-                   :api="api"
-                   :entity-id-key="entityIdKey"
-                   :entity-param-key="entityParamKey"
-                   :show-route-name="showRouteName"
-                   :show-close-button="false"
-                   :show-edit-button="false"
-                   :show-expand-button="false"
-                   :show-save-button="false"
-                   :show-reload-button="false" />
+    <q-card class="form"
+            flat>
+      <entity-create v-if="mounted"
+                     ref="entityCreate"
+                     v-model:value="inputs"
+                     title=""
+                     :api="api"
+                     :entity-id-key="entityIdKey"
+                     :entity-param-key="entityParamKey"
+                     :show-route-name="showRouteName"
+                     :default-layout="false"
+                     :show-close-button="false"
+                     :show-edit-button="false"
+                     :show-expand-button="false"
+                     :show-save-button="false"
+                     :show-reload-button="false" />
+      <div class="action">
+        <div class="row q-mt-lg justify-end">
+          <div class="col-md-4 col-12">
+            <q-btn color="primary"
+                   class="full-width"
+                   :loading="entityLoading"
+                   @click="create">
+              انتشار
+            </q-btn>
+          </div>
+        </div>
+      </div>
+    </q-card>
   </div>
 </template>
 
@@ -30,51 +49,91 @@ import { shallowRef } from 'vue'
 import { EntityCreate } from 'quasar-crud'
 import { mixinWidget } from 'src/mixin/Mixins.js'
 import { APIGateway } from 'src/api/APIGateway.js'
-import BtnControl from 'src/components/Control/btn.vue'
-import ContentMedias from 'src/components/FormBuilderCustumComponents/ContentMedias/ContentMedias.vue'
-import ContentCategorySelector from 'src/components/FormBuilderCustumComponents/ContentCategorySelector.vue'
+import PostMixin from 'src/components/Widgets/Post/PostMixin.js'
+import PostCategorySelector from 'src/components/FormBuilderCustomControls/PostCategorySelector.vue'
+import FormBuilderTiptapEditor from 'src/components/FormBuilderCustomControls/FormBuilderTiptapEditor.vue'
+// import { FormBuilderAssist } from 'quasar-form-builder'
 
-const BtnControlComp = shallowRef(BtnControl)
-const ContentMediasComp = shallowRef(ContentMedias)
-const ContentCategorySelectorComp = shallowRef(ContentCategorySelector)
+const PostCategorySelectorComp = shallowRef(PostCategorySelector)
+const FormBuilderTiptapEditorComp = shallowRef(FormBuilderTiptapEditor)
 
 export default {
-  name: 'AdminContentCreate',
+  name: 'AdminPostCreate',
   components: { EntityCreate },
-  mixins: [mixinWidget],
-  data: () => {
+  mixins: [mixinWidget, PostMixin],
+  data () {
+    const authorization = 'Bearer ' + this.$store.getters['Auth/accessToken']
+    const uploadVideo = this.uploadVideo
+    const uploadAudio = this.uploadAudio
     return {
       mounted: false,
       entityLoading: false,
-      api: APIGateway.content.APIAdresses.base,
+      api: APIGateway.post.APIAdresses.base,
       entityIdKey: 'id',
       entityParamKey: 'id',
-      showRouteName: 'Admin.Content.Show',
+      showRouteName: 'AdminPanel.Post.Show',
       inputs: [
-        { type: 'input', name: 'title', responseKey: 'title', label: 'عنوان', placeholder: ' ', col: 'col-md-6 col-12' },
+        { type: 'input', name: 'title', responseKey: 'title', label: 'عنوان', placeholder: ' ', col: 'col-12' },
+        { type: 'input', name: 'order', responseKey: 'order', label: 'ترتیب', placeholder: ' ', col: 'col-md-6 col-12' },
         { type: 'file', name: 'thumbnail', responseKey: 'thumbnail', label: 'عکس', placeholder: ' ', col: 'col-md-6 col-12' },
-        { type: ContentCategorySelectorComp, name: 'category', responseKey: 'category', col: 'col-12' },
-        { type: 'inputEditor', name: 'description', responseKey: 'description', label: 'توضیحات', col: 'col-12' },
-        { type: ContentMediasComp, name: 'medias', responseKey: 'medias_info', col: 'col-12' },
-        { type: BtnControlComp, name: 'btn', responseKey: 'btn', label: 'ایجاد محتوا', placeholder: ' ', atClick: () => {}, col: 'col-md-6' }
+        { type: PostCategorySelectorComp, name: 'category', responseKey: 'category', col: 'col-12' },
+        {
+          // type: 'tiptap-editor',
+          type: FormBuilderTiptapEditorComp,
+          name: 'summary',
+          responseKey: 'summary',
+          label: 'خلاصه',
+          options: {
+            bubbleMenu: false,
+            floatingMenu: false,
+            poem: false,
+            reading: false,
+            loadBareHtml: true,
+            persianKeyboard: true,
+            mathliveOptions: { smartFence: false },
+            uploadServer: {
+              url: '/api' + APIGateway.media.APIAdresses.base,
+              instantUpload: true,
+              responseKey: 'file',
+              headers: { Authorization: authorization }
+            },
+            uploadVideo,
+            uploadAudio
+          },
+          col: 'col-md-12 col-12'
+        },
+        {
+          // type: 'tiptap-editor',
+          type: FormBuilderTiptapEditorComp,
+          name: 'text',
+          responseKey: 'text',
+          label: 'متن',
+          options: {
+            bubbleMenu: false,
+            floatingMenu: false,
+            poem: false,
+            reading: false,
+            loadBareHtml: true,
+            persianKeyboard: true,
+            mathliveOptions: { smartFence: false },
+            uploadServer: {
+              url: '/api' + APIGateway.media.APIAdresses.base,
+              instantUpload: true,
+              responseKey: 'file',
+              headers: { Authorization: authorization }
+            },
+            uploadVideo,
+            uploadAudio
+          },
+          col: 'col-md-12 col-12'
+        }
       ]
     }
   },
-  mounted() {
-    this.setActionBtn()
+  mounted () {
     this.mounted = true
   },
   methods: {
-    setActionBtn () {
-      this.inputs.forEach((item, index) => {
-        if (item.name === 'btn') {
-          this.inputs[index].atClick = this.onSubmit
-        }
-      })
-    },
-    onSubmit () {
-      this.create()
-    },
     create() {
       this.entityLoading = true
       this.$refs.entityCreate.createEntity()
@@ -90,7 +149,7 @@ export default {
 </script>
 
 <style scoped lang="scss">
-.AdminContentCreate {
+.AdminPostCreate {
   .title {
     font-style: normal;
     font-weight: 700;

+ 119 - 0
src/components/Widgets/Content/AdminContentList-old/AdminContentList.vue

@@ -0,0 +1,119 @@
+<template>
+  <div class="AdminContentList"
+       :style="localOptions.style">
+    <div class="more-action">
+      <q-btn label="محتوای جدید"
+             color="primary"
+             :to="{name: 'Admin.Content.Create'}" />
+    </div>
+    <entity-index v-if="mounted"
+                  v-model:value="inputs"
+                  title="لیست محتواها"
+                  :api="api"
+                  :table="table"
+                  :table-keys="tableKeys"
+                  :create-route-name="createRouteName"
+                  :show-search-button="false"
+                  :show-expand-button="false"
+                  :show-reload-button="false">
+      <template #entity-index-table-cell="{inputData}">
+        <template v-if="inputData.col.name === 'number'">
+          {{ inputData.rowNumber }}
+        </template>
+        <template v-else-if="inputData.col.name === 'thumbnail'">
+          <q-img :src="inputData.col.value"
+                 width="100px" />
+        </template>
+        <template v-else-if="inputData.col.name === 'action'">
+          <q-btn color="primary"
+                 :to="{name: 'Admin.Content.Show', params: {id: inputData.props.row.id}}">
+            مشاهده جزییات
+          </q-btn>
+        </template>
+        <template v-else>
+          {{ inputData.col.value }}
+        </template>
+      </template>
+    </entity-index>
+  </div>
+</template>
+
+<script>
+import { EntityIndex } from 'quasar-crud'
+import { mixinWidget } from 'src/mixin/Mixins.js'
+import { APIGateway } from 'src/api/APIGateway.js'
+
+export default {
+  name: 'AdminContentList',
+  components: { EntityIndex },
+  mixins: [mixinWidget],
+  data: () => {
+    return {
+      api: APIGateway.content.APIAdresses.base,
+      tableKeys: {
+        data: 'results',
+        total: 'count',
+        currentPage: 'current',
+        perPage: 'per_page',
+        pageKey: 'page'
+      },
+      inputs: [],
+      table: {
+        columns: [
+          {
+            name: 'number',
+            required: true,
+            label: 'شماره',
+            align: 'left',
+            field: () => ''
+          },
+          {
+            name: 'thumbnail',
+            required: true,
+            label: 'تصویر',
+            align: 'left',
+            field: row => row.thumbnail
+          },
+          {
+            name: 'title',
+            required: true,
+            label: 'عنوان',
+            align: 'left',
+            field: row => row.title
+          },
+          {
+            name: 'category_info',
+            required: true,
+            label: 'دسته',
+            align: 'left',
+            field: row => row.category_info?.title
+          },
+          {
+            name: 'action',
+            required: true,
+            label: 'جزییات',
+            align: 'left',
+            field: row => ''
+          }
+        ]
+      },
+      mounted: false,
+      createRouteName: ''
+    }
+  },
+  mounted() {
+    this.mounted = true
+  }
+}
+</script>
+
+<style scoped lang="scss">
+.AdminContentList {
+  .more-action {
+    display: flex;
+    flex-flow: row;
+    justify-content: flex-end;
+    margin-bottom: 10px;
+  }
+}
+</style>

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

+ 70 - 41
src/components/Widgets/Content/AdminContentList/AdminContentList.vue

@@ -1,40 +1,48 @@
 <template>
-  <div class="AdminContentList"
+  <div class="AdminPostList"
        :style="localOptions.style">
-    <div class="more-action">
-      <q-btn label="محتوای جدید"
-             color="primary"
-             :to="{name: 'Admin.Content.Create'}" />
+    <div class="title">
+      نوشته ها
+      <div class="back-action">
+        <q-btn :to="{name: 'AdminPanel.Post.Create'}"
+               color="primary">
+          نوشتن مقاله جدید
+        </q-btn>
+      </div>
     </div>
-    <entity-index v-if="mounted"
-                  v-model:value="inputs"
-                  title="لیست محتواها"
-                  :api="api"
-                  :table="table"
-                  :table-keys="tableKeys"
-                  :create-route-name="createRouteName"
-                  :show-search-button="false"
-                  :show-expand-button="false"
-                  :show-reload-button="false">
-      <template #entity-index-table-cell="{inputData}">
-        <template v-if="inputData.col.name === 'number'">
-          {{ inputData.rowNumber }}
+    <q-card class="list"
+            flat>
+      <entity-index v-if="mounted"
+                    v-model:value="inputs"
+                    title=""
+                    :api="api"
+                    :table="table"
+                    :table-keys="tableKeys"
+                    :create-route-name="createRouteName"
+                    :default-layout="false"
+                    :show-search-button="false"
+                    :show-expand-button="false"
+                    :show-reload-button="false">
+        <template #entity-index-table-cell="{inputData}">
+          <template v-if="inputData.col.name === 'number'">
+            {{ inputData.rowNumber }}
+          </template>
+          <template v-else-if="inputData.col.name === 'thumbnail'">
+            <q-img :src="inputData.col.value"
+                   width="100px" />
+          </template>
+          <template v-else-if="inputData.col.name === 'action'">
+            <q-btn color="primary"
+                   :to="{name: 'AdminPanel.Post.Show', params: {id: inputData.props.row.id}}">
+              مشاهده جزییات
+            </q-btn>
+          </template>
+          <template v-else>
+            {{ inputData.col.value }}
+          </template>
         </template>
-        <template v-else-if="inputData.col.name === 'thumbnail'">
-          <q-img :src="inputData.col.value"
-                 width="100px" />
-        </template>
-        <template v-else-if="inputData.col.name === 'action'">
-          <q-btn color="primary"
-                 :to="{name: 'Admin.Content.Show', params: {id: inputData.props.row.id}}">
-            مشاهده جزییات
-          </q-btn>
-        </template>
-        <template v-else>
-          {{ inputData.col.value }}
-        </template>
-      </template>
-    </entity-index>
+      </entity-index>
+    </q-card>
   </div>
 </template>
 
@@ -44,12 +52,12 @@ import { mixinWidget } from 'src/mixin/Mixins.js'
 import { APIGateway } from 'src/api/APIGateway.js'
 
 export default {
-  name: 'AdminContentList',
+  name: 'AdminPostList',
   components: { EntityIndex },
   mixins: [mixinWidget],
   data: () => {
     return {
-      api: APIGateway.content.APIAdresses.base,
+      api: APIGateway.post.APIAdresses.base,
       tableKeys: {
         data: 'results',
         total: 'count',
@@ -108,12 +116,33 @@ export default {
 </script>
 
 <style scoped lang="scss">
-.AdminContentList {
-  .more-action {
-    display: flex;
-    flex-flow: row;
-    justify-content: flex-end;
-    margin-bottom: 10px;
+.AdminPostList {
+  .title {
+    font-style: normal;
+    font-weight: 700;
+    font-size: 24px;
+    line-height: 140%;
+    color: #424242;
+    margin-bottom: 27px;
+    position: relative;
+    .back-action {
+      position: absolute;
+      right: 0;
+      top: 0;
+    }
+  }
+  :deep(.list) {
+    .quasar-crud-index-table {
+      padding: 0;
+      .q-table__container {
+        background-color: transparent;
+        box-shadow: none;
+        border: none;
+        .q-table__top {
+          display: none;
+        }
+      }
+    }
   }
 }
 </style>

+ 162 - 0
src/components/Widgets/Content/AdminContentShow-old/AdminContentShow.vue

@@ -0,0 +1,162 @@
+<template>
+  <div class="AdminContentShow"
+       :style="localOptions.style">
+    <div class="flex justify-end">
+      <q-btn flat
+             color="grey"
+             @click="$router.go(-1)">
+        بازگشت
+        >
+      </q-btn>
+    </div>
+    <q-card flat>
+      <q-tabs v-model="tab"
+              align="left">
+        <q-tab name="content"
+               label="جزییات محتوا" />
+        <q-tab name="contentTag"
+               label="فیش برداری" />
+      </q-tabs>
+    </q-card>
+    <q-tab-panels v-if="mounted"
+                  v-model="tab">
+      <q-tab-panel name="content"
+                   class="q-pa-none">
+        <entity-edit ref="entityEdit"
+                     v-model:value="inputs"
+                     title="اطلاعات محتوا"
+                     :api="api"
+                     :entity-id-key="entityIdKey"
+                     :entity-param-key="entityParamKey"
+                     :show-route-name="showRouteName"
+                     :show-close-button="false"
+                     :show-edit-button="false"
+                     :show-expand-button="false"
+                     :show-save-button="false"
+                     :show-reload-button="false"
+                     :redirect-after-edit="false"
+                     :after-load-input-data="afterLoadInputData" />
+      </q-tab-panel>
+      <q-tab-panel name="contentTag"
+                   class="q-pa-none">
+        <admin-content-topic-create :content="contentId"
+                                    :redirect="false"
+                                    @created="onTopicCreated" />
+        <q-separator class="q-my-md" />
+        <admin-content-topic-list :key="topicListKey"
+                                  :content-id="contentId" />
+      </q-tab-panel>
+    </q-tab-panels>
+  </div>
+</template>
+
+<script>
+import { shallowRef } from 'vue'
+import { EntityEdit } from 'quasar-crud'
+import { mixinWidget } from 'src/mixin/Mixins.js'
+import { APIGateway } from 'src/api/APIGateway.js'
+import BtnControl from 'src/components/Control/btn.vue'
+import ContentMedias from 'src/components/FormBuilderCustumComponents/ContentMedias/ContentMedias.vue'
+import ContentCategorySelector from 'src/components/FormBuilderCustumComponents/ContentCategorySelector.vue'
+import AdminContentTopicList from 'src/components/Widgets/Content/AdminContentTopicList/AdminContentTopicList.vue'
+import AdminContentTopicCreate from 'src/components/Widgets/Content/AdminContentTopicCreate/AdminContentTopicCreate.vue'
+
+const BtnControlComp = shallowRef(BtnControl)
+const ContentMediasComp = shallowRef(ContentMedias)
+const ContentCategorySelectorComp = shallowRef(ContentCategorySelector)
+
+export default {
+  name: 'AdminContentShow',
+  components: {
+    EntityEdit,
+    AdminContentTopicList,
+    AdminContentTopicCreate
+  },
+  mixins: [mixinWidget],
+  data () {
+    return {
+      mounted: false,
+      entityLoading: true,
+      topicListKey: Date.now(),
+      api: APIGateway.content.APIAdresses.byId(this.$route.params.id),
+      tab: 'content',
+      entityIdKey: 'id',
+      entityParamKey: 'id',
+      showRouteName: 'Admin.Content.Show',
+      indexRouteName: 'Admin.Content.List',
+      inputs: [
+        { type: 'input', name: 'title', responseKey: 'title', label: 'عنوان', placeholder: ' ', col: 'col-md-6 col-12' },
+        { type: 'file', name: 'thumbnail', responseKey: 'thumbnail', label: 'عکس', placeholder: ' ', col: 'col-md-6 col-12' },
+        { type: ContentCategorySelectorComp, name: 'category', responseKey: 'category_info', col: 'col-md-12 col-12' },
+        { type: 'inputEditor', name: 'description', responseKey: 'description', label: 'توضیحات', col: 'col-md-12 col-12' },
+        { type: ContentMediasComp, name: 'medias', responseKey: 'medias_info', col: 'col-md-12 col-12' },
+        { type: BtnControlComp, name: 'btn', responseKey: 'btn', label: 'ویرایش محتوا', placeholder: ' ', ignoreValue: true, atClick: this.edit, col: 'col-md-6' }
+      ]
+    }
+  },
+  computed: {
+    contentId () {
+      return parseInt(this.$route.params.id)
+    }
+  },
+  mounted() {
+    this.mounted = true
+  },
+  methods: {
+    afterLoadInputData () {
+      this.entityLoading = false
+    },
+    edit() {
+      this.entityLoading = true
+      this.$refs.entityEdit.editEntity(false)
+        .then(() => {
+          this.$refs.entityEdit.getData()
+          this.entityLoading = false
+        })
+        .catch(() => {
+          this.$refs.entityEdit.getData()
+          this.entityLoading = false
+        })
+    },
+    onTopicCreated () {
+      this.topicListKey = Date.now()
+    }
+  }
+}
+</script>
+
+<style scoped lang="scss">
+.AdminContentShow {
+  .more-action {
+    display: flex;
+    flex-flow: row;
+    justify-content: flex-start;
+    margin-bottom: 10px;
+  }
+  .title {
+    font-style: normal;
+    font-weight: 700;
+    font-size: 24px;
+    line-height: 140%;
+    color: #424242;
+    margin-bottom: 27px;
+    display: flex;
+    flex-flow: row;
+    position: relative;
+    .static-title {
+
+    }
+    .dynamic-title {
+
+    }
+    .back-action {
+      position: absolute;
+      right: 0;
+      top: 0;
+    }
+  }
+  :deep(.form) {
+    padding: 24px;
+  }
+}
+</style>

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

+ 93 - 75
src/components/Widgets/Content/AdminContentShow/AdminContentShow.vue

@@ -1,5 +1,5 @@
 <template>
-  <div class="AdminContentShow"
+  <div class="AdminPostShow"
        :style="localOptions.style">
     <div class="flex justify-end">
       <q-btn flat
@@ -9,44 +9,21 @@
         >
       </q-btn>
     </div>
-    <q-card flat>
-      <q-tabs v-model="tab"
-              align="left">
-        <q-tab name="content"
-               label="جزییات محتوا" />
-        <q-tab name="contentTag"
-               label="فیش برداری" />
-      </q-tabs>
-    </q-card>
-    <q-tab-panels v-if="mounted"
-                  v-model="tab">
-      <q-tab-panel name="content"
-                   class="q-pa-none">
-        <entity-edit ref="entityEdit"
-                     v-model:value="inputs"
-                     title="اطلاعات محتوا"
-                     :api="api"
-                     :entity-id-key="entityIdKey"
-                     :entity-param-key="entityParamKey"
-                     :show-route-name="showRouteName"
-                     :show-close-button="false"
-                     :show-edit-button="false"
-                     :show-expand-button="false"
-                     :show-save-button="false"
-                     :show-reload-button="false"
-                     :redirect-after-edit="false"
-                     :after-load-input-data="afterLoadInputData" />
-      </q-tab-panel>
-      <q-tab-panel name="contentTag"
-                   class="q-pa-none">
-        <admin-content-topic-create :content="contentId"
-                                    :redirect="false"
-                                    @created="onTopicCreated" />
-        <q-separator class="q-my-md" />
-        <admin-content-topic-list :key="topicListKey"
-                                  :content-id="contentId" />
-      </q-tab-panel>
-    </q-tab-panels>
+    <entity-edit v-if="mounted"
+                 ref="entityEdit"
+                 v-model:value="inputs"
+                 title="اطلاعات محتوا"
+                 :api="api"
+                 :entity-id-key="entityIdKey"
+                 :entity-param-key="entityParamKey"
+                 :show-route-name="showRouteName"
+                 :show-close-button="false"
+                 :show-edit-button="false"
+                 :show-expand-button="false"
+                 :show-save-button="false"
+                 :show-reload-button="false"
+                 :redirect-after-edit="false"
+                 :after-load-input-data="afterLoadInputData" />
   </div>
 </template>
 
@@ -56,53 +33,103 @@ import { EntityEdit } from 'quasar-crud'
 import { mixinWidget } from 'src/mixin/Mixins.js'
 import { APIGateway } from 'src/api/APIGateway.js'
 import BtnControl from 'src/components/Control/btn.vue'
-import ContentMedias from 'src/components/FormBuilderCustumComponents/ContentMedias/ContentMedias.vue'
-import ContentCategorySelector from 'src/components/FormBuilderCustumComponents/ContentCategorySelector.vue'
-import AdminContentTopicList from 'src/components/Widgets/Content/AdminContentTopicList/AdminContentTopicList.vue'
-import AdminContentTopicCreate from 'src/components/Widgets/Content/AdminContentTopicCreate/AdminContentTopicCreate.vue'
+import PostMixin from 'src/components/Widgets/Post/PostMixin.js'
+import PostCategorySelector from 'src/components/FormBuilderCustomControls/PostCategorySelector.vue'
+import FormBuilderTiptapEditor from 'src/components/FormBuilderCustomControls/FormBuilderTiptapEditor.vue'
 
 const BtnControlComp = shallowRef(BtnControl)
-const ContentMediasComp = shallowRef(ContentMedias)
-const ContentCategorySelectorComp = shallowRef(ContentCategorySelector)
+const PostCategorySelectorComp = shallowRef(PostCategorySelector)
+const FormBuilderTiptapEditorComp = shallowRef(FormBuilderTiptapEditor)
 
 export default {
-  name: 'AdminContentShow',
+  name: 'AdminPostShow',
   components: {
-    EntityEdit,
-    AdminContentTopicList,
-    AdminContentTopicCreate
+    EntityEdit
   },
-  mixins: [mixinWidget],
+  mixins: [mixinWidget, PostMixin],
   data () {
+    const postId = this.$route.params.id
+    const uploadVideo = this.uploadVideo
+    const uploadPhoto = this.uploadPhoto
+    const uploadAudio = this.uploadAudio
     return {
       mounted: false,
       entityLoading: true,
-      topicListKey: Date.now(),
-      api: APIGateway.content.APIAdresses.byId(this.$route.params.id),
-      tab: 'content',
+      api: APIGateway.post.APIAdresses.byId(postId),
       entityIdKey: 'id',
       entityParamKey: 'id',
-      showRouteName: 'Admin.Content.Show',
-      indexRouteName: 'Admin.Content.List',
+      showRouteName: 'AdminPanel.Post.Show',
+      indexRouteName: 'AdminPanel.Post.List',
       inputs: [
-        { type: 'input', name: 'title', responseKey: 'title', label: 'عنوان', placeholder: ' ', col: 'col-md-6 col-12' },
+        { type: 'input', name: 'title', responseKey: 'title', label: 'عنوان', placeholder: ' ', col: 'col-12' },
+        { type: 'input', name: 'order', responseKey: 'order', label: 'ترتیب', placeholder: ' ', col: 'col-md-6 col-12' },
         { type: 'file', name: 'thumbnail', responseKey: 'thumbnail', label: 'عکس', placeholder: ' ', col: 'col-md-6 col-12' },
-        { type: ContentCategorySelectorComp, name: 'category', responseKey: 'category_info', col: 'col-md-12 col-12' },
-        { type: 'inputEditor', name: 'description', responseKey: 'description', label: 'توضیحات', col: 'col-md-12 col-12' },
-        { type: ContentMediasComp, name: 'medias', responseKey: 'medias_info', col: 'col-md-12 col-12' },
-        { type: BtnControlComp, name: 'btn', responseKey: 'btn', label: 'ویرایش محتوا', placeholder: ' ', ignoreValue: true, atClick: this.edit, col: 'col-md-6' }
+        { type: PostCategorySelectorComp, name: 'category', responseKey: 'category_info', col: 'col-md-12 col-12' },
+        {
+          // type: 'tiptap-editor',
+          type: FormBuilderTiptapEditorComp,
+          name: 'summary',
+          responseKey: 'summary',
+          label: 'خلاصه',
+          options: {
+            loadBareHtml: true,
+            persianKeyboard: true,
+            uploadPhoto,
+            uploadVideo,
+            uploadAudio
+          },
+          col: 'col-md-12 col-12'
+        },
+        {
+          // type: 'tiptap-editor',
+          type: FormBuilderTiptapEditorComp,
+          name: 'text',
+          responseKey: 'text',
+          label: 'متن',
+          options: {
+            loadBareHtml: true,
+            persianKeyboard: true,
+            uploadPhoto,
+            uploadVideo,
+            uploadAudio
+          },
+          col: 'col-md-12 col-12'
+        },
+        {
+          type: BtnControlComp,
+          name: 'btn',
+          responseKey: 'btn',
+          label: 'ویرایش پست',
+          placeholder: ' ',
+          ignoreValue: true,
+          atClick: () => {
+            this.edit()
+          },
+          col: 'col-md-6'
+        },
+        {
+          type: BtnControlComp,
+          name: 'btnShowPost',
+          responseKey: 'btn',
+          label: 'مشاهده پست',
+          placeholder: ' ',
+          ignoreValue: true,
+          atClick: () => {
+            this.showPost()
+          },
+          col: 'col-md-6'
+        }
       ]
     }
   },
-  computed: {
-    contentId () {
-      return parseInt(this.$route.params.id)
-    }
-  },
   mounted() {
     this.mounted = true
   },
   methods: {
+    showPost () {
+      const routeData = this.$router.resolve({ name: 'Public.Post.Show', params: { id: this.$route.params.id } })
+      window.open(routeData.href, '_blank')
+    },
     afterLoadInputData () {
       this.entityLoading = false
     },
@@ -117,22 +144,13 @@ export default {
           this.$refs.entityEdit.getData()
           this.entityLoading = false
         })
-    },
-    onTopicCreated () {
-      this.topicListKey = Date.now()
     }
   }
 }
 </script>
 
 <style scoped lang="scss">
-.AdminContentShow {
-  .more-action {
-    display: flex;
-    flex-flow: row;
-    justify-content: flex-start;
-    margin-bottom: 10px;
-  }
+.AdminPostShow {
   .title {
     font-style: normal;
     font-weight: 700;

+ 116 - 53
src/components/Widgets/Content/ContentList/ContentList.vue

@@ -1,32 +1,23 @@
 <template>
-  <div class="ContentList"
+  <div class="PostList"
        :style="localOptions.style">
-    <div class="more-action">
-      <q-btn label="ایجاد درخواست"
-             color="primary"
-             :to="{name: 'UserPanel.Ticket.Create'}" />
-    </div>
+    <breadcrumbs class="q-mb-xl"
+                 style="margin-top: 29px; margin-bottom: 19px;" />
     <entity-index v-if="mounted"
                   ref="entityIndex"
                   v-model:value="inputs"
-                  title="لیست درخواست ها"
                   :api="api"
                   :table="table"
                   :table-keys="tableKeys"
-                  :create-route-name="createRouteName"
+                  :table-grid-size="true"
+                  :default-layout="false"
                   :show-search-button="false"
                   :show-expand-button="false"
                   :show-reload-button="false">
-      <template #entity-index-table-cell="{inputData}">
-        <template v-if="inputData.col.name === 'action'">
-          <q-btn color="primary"
-                 :to="{name: 'UserPanel.Ticket.Show', params: {id: inputData.props.row.id}}">
-            مشاهده جزییات
-          </q-btn>
-        </template>
-        <template v-else>
-          {{ inputData.col.value }}
-        </template>
+      <template #entity-index-table-item-cell="{inputData}">
+        <post-item :post="getPost(inputData.props.row)"
+                   :post-show-route-name="localOptions.postShowRouteName"
+                   :btn-show-post-class-name="this.localOptions.btnShowPostClassName" />
       </template>
     </entity-index>
   </div>
@@ -34,17 +25,21 @@
 
 <script>
 import { EntityIndex } from 'quasar-crud'
-import { Ticket } from 'src/models/Ticket.js'
+import { Post } from 'src/models/Post.js'
 import { mixinWidget } from 'src/mixin/Mixins.js'
 import { APIGateway } from 'src/api/APIGateway.js'
+import PostItem from 'src/components/PostItem.vue'
+import { FormBuilderAssist } from 'quasar-form-builder'
+import { ContentCategory } from 'src/models/ContentCategory.js'
+import Breadcrumbs from 'src/components/Widgets/Breadcrumbs/Breadcrumbs.vue'
 
 export default {
-  name: 'ContentList',
-  components: { EntityIndex },
+  name: 'PostList',
+  components: { PostItem, EntityIndex, Breadcrumbs },
   mixins: [mixinWidget],
-  data: () => {
+  data () {
     return {
-      api: APIGateway.ticket.APIAdresses.base,
+      api: APIGateway.content.APIAdresses.base,
       tableKeys: {
         data: 'results',
         total: 'count',
@@ -52,51 +47,119 @@ export default {
         perPage: 'per_page',
         pageKey: 'page'
       },
-      inputs: [],
+      inputs: [
+        { type: 'hidden', name: 'category', value: null },
+        { type: 'hidden', name: 'category__parent', value: null },
+        { type: 'hidden', name: 'category__parent__parent', value: 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
-          },
+        columns: []
+      },
+      mounted: false,
+      contentCategory: new ContentCategory(),
+      breadcrumbs: {
+        visible: true,
+        loading: false,
+        path: [
           {
-            name: 'status',
-            required: true,
-            label: 'وضعیت',
-            align: 'left',
-            field: row => (new Ticket(row)).status_info.label
+            label: 'خانه',
+            to: { name: 'Public.Home' }
           },
           {
-            name: 'action',
-            required: true,
-            label: 'جزییات',
-            align: 'left',
-            field: row => ''
+            label: 'یادداشت ها و مقالات',
+            to: { name: 'Public.Post.List' }
           }
         ]
       },
-      mounted: false,
-      createRouteName: ''
+      defaultOptions: {
+        postShowRouteName: 'Public.Post.Show',
+        categoryId: null,
+        categoryCategoryId: null,
+        categoryCategoryCategoryId: null,
+        btnShowPostClassName: null,
+        categoryFromQueryParams: false,
+        categoryCategoryFromQueryParams: false,
+        categoryCategoryCategoryFromQueryParams: false
+      }
+    }
+  },
+  computed: {
+    contentCategoryId () {
+      if (this.localOptions.categoryFromQueryParams && this.$route.params?.category_id) {
+        return this.$route.params.category_id
+      }
+
+      if (this.localOptions.categoryId) {
+        return this.localOptions.categoryId
+      }
+
+      return null
     }
   },
-  mounted() {
+  mounted () {
     this.mounted = true
+    this.$store.commit('AppLayout/updateBreadcrumbs', this.breadcrumbs)
+
+    if (this.localOptions.categoryCategoryCategoryFromQueryParams) {
+      FormBuilderAssist.setAttributeByName(this.inputs, 'category__parent__parent', 'value', this.$route.params.category_parent_parent_id)
+    } else if (this.localOptions.categoryCategoryCategoryId) {
+      FormBuilderAssist.setAttributeByName(this.inputs, 'category__parent__parent', 'value', this.localOptions.categoryCategoryCategoryId)
+    }
+
+    if (this.localOptions.categoryCategoryFromQueryParams) {
+      FormBuilderAssist.setAttributeByName(this.inputs, 'category__parent', 'value', this.$route.params.category_parent_id)
+    } else if (this.localOptions.categoryCategoryId) {
+      FormBuilderAssist.setAttributeByName(this.inputs, 'category__parent', 'value', this.localOptions.categoryCategoryId)
+    }
+
+    if (this.localOptions.categoryFromQueryParams) {
+      FormBuilderAssist.setAttributeByName(this.inputs, 'category', 'value', this.$route.params.category_id)
+    } else if (this.localOptions.categoryId) {
+      FormBuilderAssist.setAttributeByName(this.inputs, 'category', 'value', this.localOptions.categoryId)
+    }
+
+    if (this.contentCategoryId) {
+      this.getContentCategoryInfo(this.contentCategoryId)
+    }
+  },
+  methods: {
+    getPost (data) {
+      return new Post(data)
+    },
+    getContentCategoryInfo (contentCategoryId) {
+      this.contentCategory.loading = true
+      APIGateway.contentCategory.get({ data: { id: contentCategoryId } })
+        .then((contentCategory) => {
+          this.contentCategory = new ContentCategory(contentCategory)
+          this.breadcrumbs.path.push({
+            label: this.contentCategory.title,
+            to: { name: 'Public.ContentCategory.Show', params: { category_id: contentCategoryId } }
+          })
+          this.$store.commit('AppLayout/updateBreadcrumbs', this.breadcrumbs)
+          this.contentCategory.loading = false
+        })
+        .catch(() => {
+          this.contentCategory.loading = false
+        })
+    }
   }
 }
 </script>
 
 <style scoped lang="scss">
-.ContentList {
+.PostList {
+  :deep(.entity-index) {
+    .quasar-crud-index-table {
+      .q-table__top {
+        display: none;
+      }
+      .q-table__bottom {
+        .q-table__control {
+          display: none;
+        }
+      }
+    }
+  }
   .more-action {
     display: flex;
     flex-flow: row;

+ 67 - 2
src/components/Widgets/Content/ContentList/OptionPanel.vue

@@ -1,7 +1,58 @@
 <template>
   <option-panel-tabs v-model:options="localOptions">
     <template #main-tab>
-      ...
+      <div class="q-mt-md">
+        کد دسته پدربزرگ
+      </div>
+      <div class="row">
+        <div class="col-md-4 col-12">
+          <q-checkbox v-model="localOptions.categoryCategoryCategoryFromQueryParams"
+                      label="استفاده از کوری پارام صفحه" />
+        </div>
+        <div v-if="!localOptions.categoryCategoryCategoryFromQueryParams"
+             class="col-md-8 col-12">
+          <q-input v-model="localOptions.categoryCategoryCategoryId" />
+        </div>
+      </div>
+      <div>
+        کد دسته پدر
+      </div>
+      <div class="row">
+        <div class="col-md-4 col-12">
+          <q-checkbox v-model="localOptions.categoryCategoryFromQueryParams"
+                      label="استفاده از کوری پارام صفحه" />
+        </div>
+        <div v-if="!localOptions.categoryCategoryFromQueryParams"
+             class="col-md-8 col-12">
+          <q-input v-model="localOptions.categoryCategoryId" />
+        </div>
+      </div>
+      <div>
+        کد دسته فرزند
+      </div>
+      <div class="row">
+        <div class="col-md-4 col-12">
+          <q-checkbox v-model="localOptions.categoryFromQueryParams"
+                      label="استفاده از کوری پارام صفحه" />
+        </div>
+        <div v-if="!localOptions.categoryFromQueryParams"
+             class="col-md-8 col-12">
+          <q-input v-model="localOptions.categoryId" />
+        </div>
+      </div>
+      <div>
+        نام کلاس دکمه های موجود در کامپوننت
+      </div>
+      <div class="row">
+        <div class="col-md-8 col-12">
+          <q-input v-model="localOptions.btnShowPostClassName" />
+        </div>
+      </div>
+      <div class="q-mt-md">
+        <q-input v-model="localOptions.postShowRouteName"
+                 label="post show Route name" />
+      </div>
+
     </template>
   </option-panel-tabs>
 </template>
@@ -16,6 +67,20 @@ export default defineComponent({
   components: {
     OptionPanelTabs
   },
-  mixins: [mixinOptionPanel]
+  mixins: [mixinOptionPanel],
+  data () {
+    return {
+      defaultOptions: {
+        postShowRouteName: 'Public.Post.Show',
+        btnShowPostClassName: null,
+        categoryId: null,
+        categoryCategoryId: null,
+        categoryCategoryCategoryId: null,
+        categoryFromQueryParams: false,
+        categoryCategoryFromQueryParams: false,
+        categoryCategoryCategoryFromQueryParams: false
+      }
+    }
+  }
 })
 </script>

+ 71 - 0
src/components/Widgets/Content/ContentMixin.js

@@ -0,0 +1,71 @@
+import { APIGateway } from 'src/api/APIGateway.js'
+
+const mixinAuth = {
+  methods: {
+    uploadAudio (files, updateUploadingProgressBar, onFileUpload, onError) {
+      const formData = new FormData()
+      formData.append('file', files[0])
+      formData.append('type', 'SOUND')
+      this.$axios.post(APIGateway.media.APIAdresses.base, formData, {
+        headers: {
+          'Content-Type': 'multipart/form-data'
+        },
+        onUploadProgress: progressEvent => {
+          const { loaded, total } = progressEvent
+          const percentage = Math.floor((loaded * 100) / total)
+          updateUploadingProgressBar(percentage + '%')
+        }
+      })
+        .then((response) => {
+          onFileUpload(response.data.file)
+        })
+        .catch(() => {
+          onError()
+        })
+    },
+    uploadPhoto (files, updateUploadingProgressBar, onFileUpload, onError) {
+      const formData = new FormData()
+      formData.append('file', files[0])
+      formData.append('type', 'IMAGE')
+      this.$axios.post(APIGateway.media.APIAdresses.base, formData, {
+        headers: {
+          'Content-Type': 'multipart/form-data'
+        },
+        onUploadProgress: progressEvent => {
+          const { loaded, total } = progressEvent
+          const percentage = Math.floor((loaded * 100) / total)
+          updateUploadingProgressBar(percentage + '%')
+        }
+      })
+        .then((response) => {
+          onFileUpload(response.data.file)
+        })
+        .catch(() => {
+          onError()
+        })
+    },
+    uploadVideo (files, updateUploadingProgressBar, onFileUpload, onError) {
+      const formData = new FormData()
+      formData.append('file', files[0])
+      formData.append('type', 'VIDEO')
+      this.$axios.post(APIGateway.media.APIAdresses.base, formData, {
+        headers: {
+          'Content-Type': 'multipart/form-data'
+        },
+        onUploadProgress: progressEvent => {
+          const { loaded, total } = progressEvent
+          const percentage = Math.floor((loaded * 100) / total)
+          updateUploadingProgressBar(percentage + '%')
+        }
+      })
+        .then((response) => {
+          onFileUpload(response.data.file)
+        })
+        .catch(() => {
+          onError()
+        })
+    }
+  }
+}
+
+export default mixinAuth

+ 16 - 7
src/components/Widgets/Content/ContentShow/ContentShow.vue

@@ -6,28 +6,29 @@
       </div>
       <div ref="printArea"
            class="print-area">
-        <div class="content-title">
+        <div v-if="!content.content"
+             class="content-title">
           {{content.title}}
         </div>
 
         <q-separator class="q-mt-md" />
 
         <div class="content-body"
-             v-html="content.description" />
+             v-html="content.text" />
 
         <div class="attributes">
           <q-btn v-if="content.category_info?.parent?.parent?.id"
-                 :to="{name: 'Public.PostCategoryParentParent.Show', params: {category_id: content.category_info.parent.parent.id}}"
+                 :to="{name: 'Public.ContentCategoryParentParent.Show', params: {category_id: content.category_info.parent.parent.id}}"
                  class="attribute-item">
             {{content.category_info.parent.parent.title}}
           </q-btn>
           <q-btn v-if="content.category_info?.parent?.id"
-                 :to="{name: 'Public.PostCategoryParent.Show', params: {category_id: content.category_info.parent.id}}"
+                 :to="{name: 'Public.ContentCategoryParent.Show', params: {category_id: content.category_info.parent.id}}"
                  class="attribute-item">
             {{content.category_info.parent.title}}
           </q-btn>
           <q-btn v-if="content.category_info?.id"
-                 :to="{name: 'Public.PostCategory.Show', params: {category_id: content.category_info.id}}"
+                 :to="{name: 'Public.ContentCategory.Show', params: {category_id: content.category_info.id}}"
                  class="attribute-item">
             {{content.category_info.title}}
           </q-btn>
@@ -101,6 +102,12 @@ export default {
     },
     prefetchServerDataPromiseThen (content) {
       this.content = new Content(content)
+      this.content.content = false
+
+      // this.breadcrumbs.path.push({
+      //   label: this.content.category_info.title,
+      //   to: { name: 'Public.ContentCategory.Show', params: { category_id: this.content.category_info.id } }
+      // })
       this.breadcrumbs.path.push({
         label: this.content.title,
         to: { name: this.localOptions.contentShowRouteName, params: { id: this.content.id } }
@@ -109,6 +116,7 @@ export default {
       this.$emit('onloadn', this.content)
     },
     prefetchServerDataPromiseCatch () {
+      this.content.content = false
     },
 
     getTitledDateTime (dateTime) {
@@ -118,6 +126,7 @@ export default {
       return ShamsiDate.getDateTime(time)
     },
     getContent () {
+      this.content.content = true
       return APIGateway.content.get(this.contentId)
     },
     print () {
@@ -223,8 +232,8 @@ export default {
     font-size: 1.4rem;
   }
   .attributes {
-    display: flex;
-    flex-flow: row;
+      display: flex;
+      flex-flow: row;
     .attribute-item {
       color: #475f4a;
       background-color: #eaeaea;

+ 10 - 2
src/components/Widgets/Content/ContentShow/OptionPanel.vue

@@ -1,7 +1,8 @@
 <template>
   <option-panel-tabs v-model:options="localOptions">
     <template #main-tab>
-      ...
+      <q-input v-model="localOptions.postShowRouteName"
+               label="post show Route name" />
     </template>
   </option-panel-tabs>
 </template>
@@ -16,6 +17,13 @@ export default defineComponent({
   components: {
     OptionPanelTabs
   },
-  mixins: [mixinOptionPanel]
+  mixins: [mixinOptionPanel],
+  data () {
+    return {
+      defaultOptions: {
+        postShowRouteName: 'Public.Post.Show'
+      }
+    }
+  }
 })
 </script>

+ 61 - 0
src/components/Widgets/Content/LastContents/LastContents.vue

@@ -0,0 +1,61 @@
+<template>
+  <div class="LastBlogs row q-col-gutter-md"
+       :style="localOptions.style">
+    <div v-for="post in postList.list"
+         :key="post.id"
+         class="col-md-6 col-12">
+      <post-item :post="post" />
+    </div>
+  </div>
+</template>
+
+<script>
+import PostItem from './PostItem.vue'
+import { PostList } from 'src/models/Post.js'
+import { APIGateway } from 'src/api/APIGateway.js'
+import { mixinPrefetchServerData, mixinWidget } from 'src/mixin/Mixins.js'
+
+export default {
+  name: 'LastPosts',
+  components: {
+    PostItem
+  },
+  mixins: [mixinWidget, mixinPrefetchServerData],
+  data () {
+    return {
+      mounted: false,
+      defaultOptions: {
+        categoryId: null,
+        categoryParentId: null,
+        categoryParentParentId: 1
+      },
+      postList: new PostList()
+    }
+  },
+  mounted() {
+    this.mounted = true
+  },
+  methods: {
+    prefetchServerDataPromise () {
+      return this.getApiRequest()
+    },
+    prefetchServerDataPromiseThen ({ list }) {
+      this.postList = new PostList(list)
+      this.postList.loading = false
+    },
+    prefetchServerDataPromiseCatch () {
+      this.postList.loading = false
+    },
+
+    getApiRequest() {
+      this.postList.loading = true
+      return APIGateway.post.index({ category__parent__parent: this.localOptions.categoryParentParentId, category__parent: this.localOptions.categoryParentId, category: this.localOptions.categoryId })
+    }
+  }
+}
+</script>
+
+<style scoped lang="scss">
+.LastBlogs {
+}
+</style>

+ 35 - 0
src/components/Widgets/Content/LastContents/OptionPanel.vue

@@ -0,0 +1,35 @@
+<template>
+  <option-panel-tabs v-model:options="localOptions">
+    <template #main-tab>
+      <q-input v-model="localOptions.categoryId"
+               label="category id" />
+      <q-input v-model="localOptions.categoryParentId"
+               label="category parent id" />
+      <q-input v-model="localOptions.categoryParentParentId"
+               label="category parent parent id" />
+    </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],
+  data() {
+    return {
+      defaultOptions: {
+        categoryId: null,
+        categoryParentId: null,
+        categoryParentParentId: 1
+      }
+    }
+  }
+})
+</script>

+ 98 - 0
src/components/Widgets/Content/LastContents/PostItem.vue

@@ -0,0 +1,98 @@
+<template>
+  <q-card class="PostItem"
+          flat>
+    <div class="thumbnail">
+      <router-link :to="{name: 'Public.Post.Show', params: {id: post.id}}">
+        <q-img :src="post.thumbnail" />
+      </router-link>
+    </div>
+    <div class="texts">
+      <div class="category-title">
+        <router-link :to="{name: 'Public.Post.Show', params: {id: post.id}}">
+          {{ post.category_info.title }}
+        </router-link>
+      </div>
+      <div class="title ellipsis-2-lines">
+        <router-link :to="{name: 'Public.Post.Show', params: {id: post.id}}">
+          {{ post.title }}
+        </router-link>
+      </div>
+      <div class="published-date">
+        تاریخ انتشار
+        {{ miladiToShamsi(post.creation_time) }}
+      </div>
+    </div>
+  </q-card>
+</template>
+
+<script>
+import { Post } from 'src/models/Post.js'
+import Assist from 'src/assets/js/Assist.js'
+
+export default {
+  name: 'PostItem',
+  props: {
+    post: {
+      type: Post,
+      default: new Post()
+    }
+  },
+  methods: {
+    miladiToShamsi (date) {
+      return Assist.miladiToShamsi(date, 'jDD jMMM jYYYY')
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.PostItem {
+  display: flex;
+  flex-flow: row;
+  justify-content: flex-start;
+  border: 2px solid transparent;
+  padding: 16px;
+  &:hover {
+    border-radius: 14px;
+    border-color: #0B6AB1;
+  }
+  .thumbnail {
+    width: 148px;
+    .q-img {
+      width: 100%;
+    }
+  }
+  .texts {
+    width: calc(100% - 148px);
+    position: relative;
+    padding-left: 14px;
+    .category-title {
+      width: max-content;
+      border-radius: 3px;
+      background: #E3E7F4;
+      padding: 4px 8px;
+      color: #666;
+      font-size: 14px;
+      font-weight: 500;
+      line-height: 100%;
+      margin-bottom: 8px;
+    }
+    .title {
+      width: 100%;
+      color: #000;
+      text-align: left;
+      font-size: 18px;
+      font-weight: 700;
+    }
+    .published-date {
+      color: #000;
+      text-align: right;
+      font-size: 14px;
+      font-weight: 200;
+      position: absolute;
+      left: 14px;
+      bottom: 0;
+    }
+  }
+}
+</style>

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

@@ -132,7 +132,7 @@ export default {
           this.postCategory = new PostCategory(postCategory)
           this.breadcrumbs.path.push({
             label: this.postCategory.title,
-            to: { name: 'Public.PostCategory.Show', params: { category_id: postCategoryId } }
+            to: { name: 'Public.ContentCategory.Show', params: { category_id: postCategoryId } }
           })
           this.$store.commit('AppLayout/updateBreadcrumbs', this.breadcrumbs)
           this.postCategory.loading = false

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

@@ -131,7 +131,7 @@ export default {
 
       this.breadcrumbs.path.push({
         label: this.post.category_info.title,
-        to: { name: 'Public.PostCategory.Show', params: { category_id: this.post.category_info.id } }
+        to: { name: 'Public.ContentCategory.Show', params: { category_id: this.post.category_info.id } }
       })
       this.breadcrumbs.path.push({
         label: this.post.title,

+ 35 - 2
src/router/routes/Public.js

@@ -31,12 +31,46 @@ export default {
     {
       path: 'content',
       name: 'Public.Content',
+      meta: {
+        hasDynamicSetting: true
+      },
+      component: () => import('layouts/bareLayout.vue'),
+      children: [
+        {
+          path: '',
+          name: 'Public.Content.List',
+          component: () => import('src/pages/BasicDynamicPage.vue')
+        },
+        {
+          path: ':id',
+          name: 'Public.Content.Show',
+          component: () => import('src/pages/BasicDynamicPage.vue')
+        }
+      ]
+    },
+    {
+      path: 'content_category',
+      name: 'Public.ContentCategory',
       component: () => import('layouts/bareLayout.vue'),
       meta: {
         hasDynamicSetting: true
       },
       children: [
-        { path: ':id', name: 'Public.Content.Show', component: () => import('src/pages/Public/Content/Show.vue') }
+        {
+          path: 'parent/parent/:category_id',
+          name: 'Public.ContentCategoryParentParent.Show',
+          component: () => import('src/pages/BasicDynamicPage.vue')
+        },
+        {
+          path: 'parent/:category_id',
+          name: 'Public.ContentCategoryParent.Show',
+          component: () => import('src/pages/BasicDynamicPage.vue')
+        },
+        {
+          path: ':category_id',
+          name: 'Public.ContentCategory.Show',
+          component: () => import('src/pages/BasicDynamicPage.vue')
+        }
       ]
     },
     {
@@ -51,7 +85,6 @@ export default {
         { 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',
       path: 'all-classrooms',