| @@ -1 +1 @@ | |||||
| VUE_APP_ROOT_URL="https://api.novinplast.org/api/v1/" | |||||
| VUE_APP_ROOT_URL = http://192.168.100.126:8000/api/v1/ | |||||
| @@ -0,0 +1,8 @@ | |||||
| # Default ignored files | |||||
| /shelf/ | |||||
| /workspace.xml | |||||
| # Editor-based HTTP Client requests | |||||
| /httpRequests/ | |||||
| # Datasource local storage ignored files | |||||
| /dataSources/ | |||||
| /dataSources.local.xml | |||||
| @@ -0,0 +1,6 @@ | |||||
| <component name="InspectionProjectProfileManager"> | |||||
| <profile version="1.0"> | |||||
| <option name="myName" value="Project Default" /> | |||||
| <inspection_tool class="Eslint" enabled="true" level="WARNING" enabled_by_default="true" /> | |||||
| </profile> | |||||
| </component> | |||||
| @@ -0,0 +1,8 @@ | |||||
| <?xml version="1.0" encoding="UTF-8"?> | |||||
| <project version="4"> | |||||
| <component name="ProjectModuleManager"> | |||||
| <modules> | |||||
| <module fileurl="file://$PROJECT_DIR$/.idea/truck-admin.iml" filepath="$PROJECT_DIR$/.idea/truck-admin.iml" /> | |||||
| </modules> | |||||
| </component> | |||||
| </project> | |||||
| @@ -0,0 +1,12 @@ | |||||
| <?xml version="1.0" encoding="UTF-8"?> | |||||
| <module type="WEB_MODULE" version="4"> | |||||
| <component name="NewModuleRootManager"> | |||||
| <content url="file://$MODULE_DIR$"> | |||||
| <excludeFolder url="file://$MODULE_DIR$/.tmp" /> | |||||
| <excludeFolder url="file://$MODULE_DIR$/temp" /> | |||||
| <excludeFolder url="file://$MODULE_DIR$/tmp" /> | |||||
| </content> | |||||
| <orderEntry type="inheritedJdk" /> | |||||
| <orderEntry type="sourceFolder" forTests="false" /> | |||||
| </component> | |||||
| </module> | |||||
| @@ -0,0 +1,6 @@ | |||||
| <?xml version="1.0" encoding="UTF-8"?> | |||||
| <project version="4"> | |||||
| <component name="VcsDirectoryMappings"> | |||||
| <mapping directory="" vcs="Git" /> | |||||
| </component> | |||||
| </project> | |||||
| @@ -29,7 +29,7 @@ | |||||
| "@zhuowenli/vue-feather-icons": "^5.0.2", | "@zhuowenli/vue-feather-icons": "^5.0.2", | ||||
| "add": "^2.0.6", | "add": "^2.0.6", | ||||
| "aos": "^2.3.4", | "aos": "^2.3.4", | ||||
| "apexcharts": "^3.44.2", | |||||
| "apexcharts": "^5.3.2", | |||||
| "axios": "^1.7.9", | "axios": "^1.7.9", | ||||
| "bootstrap": "^5.3.2", | "bootstrap": "^5.3.2", | ||||
| "bootstrap-datepicker": "^1.10.0", | "bootstrap-datepicker": "^1.10.0", | ||||
| @@ -64,12 +64,14 @@ | |||||
| "vue-flatpickr-component": "^11.0.3", | "vue-flatpickr-component": "^11.0.3", | ||||
| "vue-masonry": "^0.16.0", | "vue-masonry": "^0.16.0", | ||||
| "vue-router": "^4.2.5", | "vue-router": "^4.2.5", | ||||
| "vue-simple-stepper": "^1.1.1", | |||||
| "vue3-apexcharts": "^1.4.4", | "vue3-apexcharts": "^1.4.4", | ||||
| "vue3-datepicker": "^0.4.0", | "vue3-datepicker": "^0.4.0", | ||||
| "vue3-google-map": "^0.18.0", | "vue3-google-map": "^0.18.0", | ||||
| "vue3-persian-datetime-picker": "^1.2.2", | "vue3-persian-datetime-picker": "^1.2.2", | ||||
| "vue3-select-component": "^0.11.3", | "vue3-select-component": "^0.11.3", | ||||
| "vue3-select2-component": "^0.1.7", | "vue3-select2-component": "^0.1.7", | ||||
| "vue3-steppy": "^1.5.8", | |||||
| "vue3-toastify": "^0.2.5", | "vue3-toastify": "^0.2.5", | ||||
| "vuex": "^4.1.0", | "vuex": "^4.1.0", | ||||
| "yarn": "^1.22.21" | "yarn": "^1.22.21" | ||||
| @@ -161,4 +161,32 @@ File: style.css | |||||
| @import 'themes/layouts/customizer'; | @import 'themes/layouts/customizer'; | ||||
| @import '@/assets/scss/landing.scss'; | @import '@/assets/scss/landing.scss'; | ||||
| @import '@/assets/scss/themes/custom.scss'; | |||||
| @import '@/assets/scss/themes/custom.scss'; | |||||
| .steppy-item-title { | |||||
| white-space: nowrap; | |||||
| } | |||||
| .steppy-progress-bar { | |||||
| right: 0 | |||||
| } | |||||
| .steppy-pane { | |||||
| text-align: right !important; | |||||
| box-shadow: none !important; | |||||
| padding: 0 !important; | |||||
| } | |||||
| .btn--default-2 { | |||||
| margin-right: auto; | |||||
| margin-left: unset !important; | |||||
| } | |||||
| .controls { | |||||
| display: none !important; | |||||
| } | |||||
| .wrapper-steppy { | |||||
| padding: 10px; | |||||
| } | |||||
| @@ -21,84 +21,136 @@ | |||||
| ></button> | ></button> | ||||
| </div> | </div> | ||||
| <div class="modal-body"> | <div class="modal-body"> | ||||
| <form @submit.prevent="addCat"> | |||||
| <BRow class="g-3"> | |||||
| <BCol lg="6"> | |||||
| <div class="form-group"> | |||||
| <label class="form-label">عنوان دسته</label> | |||||
| <input | |||||
| v-model="title" | |||||
| @input="clearError('title')" | |||||
| type="text" | |||||
| class="form-control" | |||||
| placeholder="عنوان دسته" | |||||
| :class="{ 'is-invalid': errors.title }" | |||||
| <Steppy | |||||
| v-model:step="step" | |||||
| :tabs="[ | |||||
| { title: 'ثبت دسته بندی', isValid: true }, | |||||
| { title: 'ترجمه ها', isValid: true }, | |||||
| ]" | |||||
| backText="قبلی" | |||||
| nextText="بعدی" | |||||
| doneText="ذخیره" | |||||
| primaryColor1="#04A9F5" | |||||
| circleSize="45" | |||||
| :finalize="addCat" | |||||
| > | |||||
| <template #1> | |||||
| <BRow class="g-3"> | |||||
| <BCol> | |||||
| <div class="form-group"> | |||||
| <label class="form-label">عنوان دسته</label> | |||||
| <input | |||||
| v-model="titleCat" | |||||
| type="text" | |||||
| class="form-control" | |||||
| placeholder="عنوان دسته" | |||||
| :class="{ 'is-invalid': errors.title }" | |||||
| @input="clearError('title')" | |||||
| /> | |||||
| <small v-if="errors.title" class="text-danger"> | |||||
| {{ errors.title }} | |||||
| </small> | |||||
| </div> | |||||
| </BCol> | |||||
| </BRow> | |||||
| <!-- Submit Buttons --> | |||||
| <div | |||||
| class="d-flex justify-content-end gap-2" | |||||
| style="margin-top: 20px" | |||||
| > | |||||
| <button :disabled="loadingStep" @click="handlerAddCategory" class="btn btn-primary"> | |||||
| <span | |||||
| v-if="loadingStep" | |||||
| class="spinner-border spinner-border-sm" | |||||
| role="status" | |||||
| aria-hidden="true" | |||||
| /> | /> | ||||
| <small v-if="errors.title" class="text-danger"> | |||||
| {{ errors.title }} | |||||
| </small> | |||||
| </div> | |||||
| </BCol> | |||||
| <BCol lg="6"> | |||||
| <div class="form-group"> | |||||
| <label class="form-label">انتخاب آیکن</label> | |||||
| <b-dropdown | |||||
| variant="outline-primary" | |||||
| class="w-100" | |||||
| @change="clearError('icon')" | |||||
| > | |||||
| <b-dropdown-item | |||||
| v-for="(icon, index) in iconData" | |||||
| :key="index" | |||||
| :value="icon.component" | |||||
| class="icon-item" | |||||
| @click="setSelectedIcon(icon.component)" | |||||
| > | |||||
| <div class="icon-container"> | |||||
| <i :class="`ph-duotone ${icon.component}`"></i> | |||||
| </div> | |||||
| </b-dropdown-item> | |||||
| </b-dropdown> | |||||
| <div v-if="selectedIcon" class="mt-2"> | |||||
| <label class="form-label">آیکن انتخاب شده:</label> | |||||
| <div class="selected-icon-container"> | |||||
| <i :class="`ph-duotone ${selectedIcon}`"></i> | |||||
| ذخیره | |||||
| </button> | |||||
| </div> | |||||
| </template> | |||||
| <template #2> | |||||
| <form @submit.prevent="addCat"> | |||||
| <BRow class="g-3"> | |||||
| <BCol lg="6"> | |||||
| <div class="form-group"> | |||||
| <label class="form-label">انتخاب زبان</label> | |||||
| <select | |||||
| v-model="locale" | |||||
| class="form-control" | |||||
| placeholder="انتخاب کنید" | |||||
| @change="handlerChangeLocale" | |||||
| > | |||||
| <option | |||||
| key="fa" | |||||
| value="fa" | |||||
| > | |||||
| فارسی | |||||
| </option> | |||||
| <option | |||||
| key="en" | |||||
| value="en" | |||||
| > | |||||
| انگلیسی | |||||
| </option> | |||||
| <option | |||||
| key="ar" | |||||
| value="ar" | |||||
| > | |||||
| عربی | |||||
| </option> | |||||
| </select> | |||||
| </div> | </div> | ||||
| </div> | |||||
| </BCol> | |||||
| <BCol lg="6"> | |||||
| <div class="form-group"> | |||||
| <label class="form-label">عنوان دسته</label> | |||||
| <input | |||||
| v-model="title" | |||||
| @input="clearError('title')" | |||||
| type="text" | |||||
| class="form-control" | |||||
| placeholder="عنوان دسته" | |||||
| :class="{ 'is-invalid': errors.title }" | |||||
| /> | |||||
| <small v-if="errors.title" class="text-danger"> | |||||
| {{ errors.title }} | |||||
| </small> | |||||
| </div> | |||||
| </BCol> | |||||
| </BRow> | |||||
| <small v-if="errors.icon" class="text-danger"> | |||||
| {{ errors.icon }} | |||||
| </small> | |||||
| <!-- Submit Buttons --> | |||||
| <div | |||||
| class="d-flex justify-content-end gap-2" | |||||
| style="margin-top: 20px" | |||||
| > | |||||
| <button | |||||
| type="button" | |||||
| class="btn btn-secondary" | |||||
| data-bs-dismiss="modal" | |||||
| id="closeAddBlogCat" | |||||
| > | |||||
| بستن | |||||
| </button> | |||||
| <button type="submit" class="btn btn-primary" :disabled="loading"> | |||||
| <span | |||||
| v-if="loading" | |||||
| class="spinner-border spinner-border-sm" | |||||
| role="status" | |||||
| aria-hidden="true" | |||||
| ></span> | |||||
| ذخیره | |||||
| </button> | |||||
| </div> | </div> | ||||
| </BCol> | |||||
| </BRow> | |||||
| <!-- Submit Buttons --> | |||||
| <div | |||||
| class="d-flex justify-content-end gap-2" | |||||
| style="margin-top: 20px" | |||||
| > | |||||
| <button | |||||
| type="button" | |||||
| class="btn btn-secondary" | |||||
| data-bs-dismiss="modal" | |||||
| id="closeAddBlogCat" | |||||
| > | |||||
| بستن | |||||
| </button> | |||||
| <button type="submit" class="btn btn-primary" :disabled="loading"> | |||||
| <span | |||||
| v-if="loading" | |||||
| class="spinner-border spinner-border-sm" | |||||
| role="status" | |||||
| aria-hidden="true" | |||||
| ></span> | |||||
| ذخیره | |||||
| </button> | |||||
| </div> | |||||
| </form> | |||||
| </form> | |||||
| </template> | |||||
| </Steppy> | |||||
| </div> | </div> | ||||
| </div> | </div> | ||||
| </div> | </div> | ||||
| @@ -111,14 +163,22 @@ import { ref } from "vue"; | |||||
| import { toast } from "vue3-toastify"; | import { toast } from "vue3-toastify"; | ||||
| import "vue3-toastify/dist/index.css"; | import "vue3-toastify/dist/index.css"; | ||||
| import ApiServiece from "@/services/ApiService"; | import ApiServiece from "@/services/ApiService"; | ||||
| import {Steppy} from "vue3-steppy"; | |||||
| export default { | export default { | ||||
| components: {Steppy}, | |||||
| props: {}, | props: {}, | ||||
| setup(props, { emit }) { | setup(props, { emit }) { | ||||
| const title = ref(); | const title = ref(); | ||||
| const selectedIcon = ref(); | const selectedIcon = ref(); | ||||
| const errors = ref({}); | const errors = ref({}); | ||||
| const loading = ref(false); | const loading = ref(false); | ||||
| const loadingStep = ref(false); | |||||
| const locale = ref('fa'); | |||||
| const editMode = ref(false) | |||||
| const titleCat = ref(null) | |||||
| const step = ref(1) | |||||
| const blogCategory = ref(null) | |||||
| const clearError = (field) => { | const clearError = (field) => { | ||||
| errors.value[field] = ""; | errors.value[field] = ""; | ||||
| @@ -127,7 +187,7 @@ export default { | |||||
| const validateForm = () => { | const validateForm = () => { | ||||
| errors.value = {}; | errors.value = {}; | ||||
| if (!title.value) errors.value.title = "وارد کردن عنوان ضروری می باشد"; | if (!title.value) errors.value.title = "وارد کردن عنوان ضروری می باشد"; | ||||
| if (!selectedIcon.value) errors.value.icon = "انتخاب آیکن ضروری است"; | |||||
| if (!locale.value) errors.value.icon = "انتخاب زبان ضروری است"; | |||||
| return Object.keys(errors.value).length === 0; | return Object.keys(errors.value).length === 0; | ||||
| }; | }; | ||||
| @@ -135,8 +195,34 @@ export default { | |||||
| selectedIcon.value = icon; | selectedIcon.value = icon; | ||||
| }; | }; | ||||
| const handlerAddCategory = async () => { | |||||
| try { | |||||
| loadingStep.value = true; | |||||
| const { data: { message, success, data } } = await ApiServiece.post( | |||||
| `admin/blog-categories`, | |||||
| { title: title.value }, | |||||
| { | |||||
| headers: { | |||||
| Authorization: `Bearer ${localStorage.getItem("token")}`, | |||||
| }, | |||||
| }) | |||||
| if (success) { | |||||
| toast.success(message) | |||||
| blogCategory.value = data?.id; | |||||
| step.value++ | |||||
| } | |||||
| } catch (e) { | |||||
| toast.error(e?.response?.data?.message) | |||||
| } finally { | |||||
| loadingStep.value = false; | |||||
| } | |||||
| } | |||||
| const addCat = () => { | const addCat = () => { | ||||
| console.log(selectedIcon.value); | |||||
| if (!validateForm()) { | if (!validateForm()) { | ||||
| toast.error("لطفا فیلد های لازم را وارد نمایید", { | toast.error("لطفا فیلد های لازم را وارد نمایید", { | ||||
| position: "top-right", | position: "top-right", | ||||
| @@ -146,12 +232,14 @@ export default { | |||||
| } | } | ||||
| loading.value = true; | loading.value = true; | ||||
| const formData = new FormData(); | |||||
| formData.append("title", title.value); | |||||
| formData.append("icon", selectedIcon.value); | |||||
| ApiServiece.post(`admin/blog-categories`, formData) | |||||
| .then(() => { | |||||
| toast.success("!دسته با موفقیت اضافه شد", { | |||||
| const params = { | |||||
| title: title.value, | |||||
| locale: locale.value, | |||||
| } | |||||
| ApiServiece.post(`admin/blog-categories/${blogCategory.value}/translations`, params) | |||||
| .then(({ data }) => { | |||||
| toast.success(data?.message, { | |||||
| position: "top-right", | position: "top-right", | ||||
| autoClose: 1000, | autoClose: 1000, | ||||
| }); | }); | ||||
| @@ -161,7 +249,7 @@ export default { | |||||
| document.getElementById("closeAddBlogCat").click(); | document.getElementById("closeAddBlogCat").click(); | ||||
| emit("cat-updated"); | emit("cat-updated"); | ||||
| title.value = ""; | title.value = ""; | ||||
| selectedIcon.value = ""; | |||||
| step.value = 1; | |||||
| }, 500); | }, 500); | ||||
| }) | }) | ||||
| .catch((error) => { | .catch((error) => { | ||||
| @@ -183,9 +271,15 @@ export default { | |||||
| addCat, | addCat, | ||||
| title, | title, | ||||
| selectedIcon, | selectedIcon, | ||||
| locale, | |||||
| iconData, | iconData, | ||||
| setSelectedIcon, | setSelectedIcon, | ||||
| editMode, | |||||
| titleCat, | |||||
| step, | |||||
| handlerAddCategory, | |||||
| loadingStep, | |||||
| blogCategory, | |||||
| }; | }; | ||||
| }, | }, | ||||
| }; | }; | ||||
| @@ -19,62 +19,63 @@ | |||||
| ></button> | ></button> | ||||
| </div> | </div> | ||||
| <div class="modal-body"> | <div class="modal-body"> | ||||
| <form @submit.prevent="editCat"> | |||||
| <form @submit.prevent="editCat" class="mt-4"> | |||||
| <BButton | |||||
| :disabled="!findLocaleTranslation" | |||||
| :loading="loadingDelete" | |||||
| @click="handlerRemoveTranslation" | |||||
| class="btn btn-sm rounded btn-danger d-block" | |||||
| style="margin-right: auto" | |||||
| > | |||||
| حذف ترجمه {{ findLocaleTranslation }} | |||||
| </BButton> | |||||
| <BRow class="g-3"> | <BRow class="g-3"> | ||||
| <BCol lg="6"> | <BCol lg="6"> | ||||
| <div class="form-group"> | <div class="form-group"> | ||||
| <label class="form-label">عنوان دسته</label> | |||||
| <input | |||||
| v-model="localTitle" | |||||
| @input="clearError('localTitle')" | |||||
| type="text" | |||||
| class="form-control" | |||||
| placeholder="عنوان دسته" | |||||
| :class="{ 'is-invalid': errors.localTitle }" | |||||
| /> | |||||
| <small v-if="errors.localTitle" class="text-danger"> | |||||
| {{ errors.localTitle }} | |||||
| </small> | |||||
| <label class="form-label">انتخاب زبان</label> | |||||
| <select | |||||
| v-model="locale" | |||||
| class="form-control" | |||||
| placeholder="انتخاب کنید" | |||||
| @change="handlerChangeLocale" | |||||
| > | |||||
| <option | |||||
| key="fa" | |||||
| value="fa" | |||||
| > | |||||
| فارسی | |||||
| </option> | |||||
| <option | |||||
| key="en" | |||||
| value="en" | |||||
| > | |||||
| انگلیسی | |||||
| </option> | |||||
| <option | |||||
| key="ar" | |||||
| value="ar" | |||||
| > | |||||
| عربی | |||||
| </option> | |||||
| </select> | |||||
| </div> | </div> | ||||
| </BCol> | </BCol> | ||||
| <BCol lg="6"> | <BCol lg="6"> | ||||
| <div class="form-group"> | <div class="form-group"> | ||||
| <label class="form-label">انتخاب آیکن</label> | |||||
| <b-dropdown | |||||
| variant="outline-primary" | |||||
| class="w-100" | |||||
| @change="clearError('icon')" | |||||
| > | |||||
| <b-dropdown-item | |||||
| v-for="(icon, index) in iconData" | |||||
| :key="index" | |||||
| :value="icon.component" | |||||
| class="icon-item" | |||||
| @click="setSelectedIcon(icon.component)" | |||||
| > | |||||
| <div class="icon-container"> | |||||
| <i :class="`ph-duotone ${icon.component}`"></i> | |||||
| </div> | |||||
| </b-dropdown-item> | |||||
| </b-dropdown> | |||||
| <div v-if="localIcon" class="mt-2"> | |||||
| <label class="form-label">آیکن انتخاب شده:</label> | |||||
| <div class="selected-icon-container"> | |||||
| <i :class="`ph-duotone ${localIcon}`"></i> | |||||
| </div> | |||||
| </div> | |||||
| <div v-if="!localIcon" class="mt-2"> | |||||
| <label class="form-label">آیکن انتخاب شده:</label> | |||||
| <div class="selected-icon-container"> | |||||
| <i :class="`ph-duotone ${localIcon}`"></i> | |||||
| </div> | |||||
| </div> | |||||
| <small v-if="errors.icon" class="text-danger"> | |||||
| {{ errors.icon }} | |||||
| <label class="form-label">عنوان دسته</label> | |||||
| <input | |||||
| v-model="title" | |||||
| @input="clearError('title')" | |||||
| type="text" | |||||
| class="form-control" | |||||
| placeholder="عنوان دسته" | |||||
| :class="{ 'is-invalid': errors.title }" | |||||
| /> | |||||
| <small v-if="errors.title" class="text-danger"> | |||||
| {{ errors.title }} | |||||
| </small> | </small> | ||||
| </div> | </div> | ||||
| </BCol> | </BCol> | ||||
| @@ -82,24 +83,24 @@ | |||||
| <!-- Submit Buttons --> | <!-- Submit Buttons --> | ||||
| <div | <div | ||||
| class="d-flex justify-content-end gap-2" | |||||
| style="margin-top: 20px" | |||||
| class="d-flex justify-content-end gap-2" | |||||
| style="margin-top: 20px" | |||||
| > | > | ||||
| <button | <button | ||||
| type="button" | |||||
| class="btn btn-secondary" | |||||
| data-bs-dismiss="modal" | |||||
| id="closeEditBlogCat" | |||||
| type="button" | |||||
| class="btn btn-secondary" | |||||
| data-bs-dismiss="modal" | |||||
| id="closeAddBlogCat" | |||||
| > | > | ||||
| بستن | بستن | ||||
| </button> | </button> | ||||
| <button type="submit" class="btn btn-primary" :disabled="loading"> | <button type="submit" class="btn btn-primary" :disabled="loading"> | ||||
| <span | |||||
| v-if="loading" | |||||
| class="spinner-border spinner-border-sm" | |||||
| role="status" | |||||
| aria-hidden="true" | |||||
| ></span> | |||||
| <span | |||||
| v-if="loading" | |||||
| class="spinner-border spinner-border-sm" | |||||
| role="status" | |||||
| aria-hidden="true" | |||||
| ></span> | |||||
| ذخیره | ذخیره | ||||
| </button> | </button> | ||||
| </div> | </div> | ||||
| @@ -138,6 +139,8 @@ export default { | |||||
| const localId = toRef(props.id); | const localId = toRef(props.id); | ||||
| const errors = ref({}); | const errors = ref({}); | ||||
| const loading = ref(false); | const loading = ref(false); | ||||
| const titleCat = ref(null); | |||||
| const title = ref(null); | |||||
| watch( | watch( | ||||
| () => props.title, | () => props.title, | ||||
| @@ -218,6 +221,8 @@ export default { | |||||
| setSelectedIcon, | setSelectedIcon, | ||||
| localIcon, | localIcon, | ||||
| localId, | localId, | ||||
| title, | |||||
| titleCat, | |||||
| }; | }; | ||||
| }, | }, | ||||
| }; | }; | ||||
| @@ -21,165 +21,158 @@ | |||||
| ></button> | ></button> | ||||
| </div> | </div> | ||||
| <div class="modal-body"> | <div class="modal-body"> | ||||
| <form @submit.prevent="addCat"> | |||||
| <BRow class="g-3"> | |||||
| <!-- Brand Title --> | |||||
| <BCol lg="6"> | |||||
| <div class="form-group"> | |||||
| <label class="form-label">عنوان دسته</label> | |||||
| <input | |||||
| v-model="title" | |||||
| @input="clearError('title')" | |||||
| type="text" | |||||
| class="form-control" | |||||
| placeholder="عنوان دسته" | |||||
| :class="{ 'is-invalid': errors.title }" | |||||
| /> | |||||
| <small v-if="errors.title" class="text-danger"> | |||||
| {{ errors.title }} | |||||
| </small> | |||||
| </div> | |||||
| </BCol> | |||||
| <!-- Brand Image Upload --> | |||||
| <BCol lg="6"> | |||||
| <div class="form-group"> | |||||
| <label class="form-label">تصویر دسته</label> | |||||
| <input | |||||
| ref="imageFileRef" | |||||
| type="file" | |||||
| accept="image/*" | |||||
| @change="handleImageChange" | |||||
| class="form-control" | |||||
| :class="{ 'is-invalid': errors.image }" | |||||
| /> | |||||
| <div v-if="imagePreview" class="mt-2"> | |||||
| <img | |||||
| :src="imagePreview" | |||||
| alt="Image Preview" | |||||
| class="img-fluid rounded shadow-sm Image-Preview" | |||||
| /> | |||||
| <button | |||||
| type="button" | |||||
| @click="removeImage()" | |||||
| class="delete-btn" | |||||
| <Steppy | |||||
| v-model:step="step" | |||||
| :tabs="[ | |||||
| { title: 'انتخاب دسته بندی', isValid: true }, | |||||
| { title: 'ترجمه ها', isValid: true }, | |||||
| ]" | |||||
| backText="قبلی" | |||||
| nextText="بعدی" | |||||
| doneText="ذخیره" | |||||
| primaryColor1="#04A9F5" | |||||
| circleSize="45" | |||||
| :loading="loadingStep" | |||||
| :finalize="addCat" | |||||
| > | |||||
| <template #1> | |||||
| <BRow> | |||||
| <BCol lg="12"> | |||||
| <div class="form-group"> | |||||
| <label class="form-label">انتخاب پدر</label> | |||||
| <select | |||||
| v-model="selectedPaernt" | |||||
| class="form-control" | |||||
| placeholder="انتخاب کنید" | |||||
| > | > | ||||
| <i class="fa fa-trash f-16"></i> | |||||
| </button> | |||||
| <option :value="''"> | |||||
| ندارد | |||||
| </option> | |||||
| <option | |||||
| v-for="parent in localParents" | |||||
| :key="parent.id" | |||||
| :value="parent.id" | |||||
| > | |||||
| {{ parent?.translation?.title ?? 'بدون نام' }} | |||||
| </option> | |||||
| </select> | |||||
| </div> | </div> | ||||
| <small v-if="errors.image" class="text-danger"> | |||||
| {{ errors.image }} | |||||
| </small> | |||||
| </div> | |||||
| </BCol> | |||||
| </BRow> | |||||
| <BRow class="g-3"> | |||||
| <!-- Brand Description --> | |||||
| <BCol lg="12"> | |||||
| <div class="form-group"> | |||||
| <label class="form-label">توضیحات دسته</label> | |||||
| <textarea | |||||
| v-model="description" | |||||
| @input="clearError('description')" | |||||
| type="text" | |||||
| class="form-control" | |||||
| placeholder="توضیحات دسته" | |||||
| :class="{ 'is-invalid': errors.description }" | |||||
| ></textarea> | |||||
| <small v-if="errors.description" class="text-danger"> | |||||
| {{ errors.description }} | |||||
| </small> | |||||
| </div> | |||||
| </BCol> | |||||
| </BRow> | |||||
| <BRow class="g-3"> | |||||
| <!-- Brand Description --> | |||||
| <BCol lg="6"> | |||||
| <div class="form-group"> | |||||
| <label class="form-label">انتخاب پدر</label> | |||||
| <select | |||||
| v-model="selectedPaernt" | |||||
| class="form-control" | |||||
| placeholder="انتخاب کنید" | |||||
| <button | |||||
| :disabled="loadingStep" | |||||
| @click="handlerSubmitCategory" | |||||
| class="btn rounded btn-primary w-auto" | |||||
| > | > | ||||
| <option :value="''"> | |||||
| ندارد | |||||
| </option> | |||||
| <option | |||||
| v-for="parent in localParents" | |||||
| :key="parent.id" | |||||
| :value="parent.id" | |||||
| > | |||||
| {{ parent.title }} | |||||
| </option> | |||||
| </select> | |||||
| </div> | |||||
| </BCol> | |||||
| <BCol lg="6"> | |||||
| <div class="form-group"> | |||||
| <label class="form-label">انتخاب آیکن</label> | |||||
| <b-dropdown | |||||
| variant="outline-primary" | |||||
| class="w-100" | |||||
| @change="clearError('icon')" | |||||
| > | |||||
| <b-dropdown-item | |||||
| v-for="(icon, index) in iconData" | |||||
| :key="index" | |||||
| :value="icon.component" | |||||
| class="icon-item" | |||||
| @click="setSelectedIcon(icon.component)" | |||||
| > | |||||
| <div class="icon-container"> | |||||
| <i :class="`ph-duotone ${icon.component}`"></i> | |||||
| </div> | |||||
| </b-dropdown-item> | |||||
| </b-dropdown> | |||||
| <div v-if="selectedIcon" class="mt-2"> | |||||
| <label class="form-label">آیکن انتخاب شده:</label> | |||||
| <div class="selected-icon-container"> | |||||
| <i :class="`ph-duotone ${selectedIcon}`"></i> | |||||
| <span | |||||
| v-if="loadingStep" | |||||
| class="spinner-border spinner-border-sm" | |||||
| role="status" | |||||
| /> | |||||
| ذخیره | |||||
| </button> | |||||
| </BCol> | |||||
| </BRow> | |||||
| </template> | |||||
| <template #2> | |||||
| <form @submit.prevent="addCat" class="mt-4"> | |||||
| <BRow class="g-3"> | |||||
| <!-- Brand Title --> | |||||
| <BCol lg="6"> | |||||
| <div class="form-group"> | |||||
| <label class="form-label">عنوان دسته</label> | |||||
| <input | |||||
| v-model="title" | |||||
| @input="clearError('title')" | |||||
| type="text" | |||||
| class="form-control" | |||||
| placeholder="عنوان دسته" | |||||
| :class="{ 'is-invalid': errors.title }" | |||||
| /> | |||||
| <small v-if="errors.title" class="text-danger"> | |||||
| {{ errors.title }} | |||||
| </small> | |||||
| </div> | </div> | ||||
| </div> | |||||
| <small v-if="errors.icon" class="text-danger"> | |||||
| {{ errors.icon }} | |||||
| </small> | |||||
| </BCol> | |||||
| <BCol lg="6"> | |||||
| <div class="form-group"> | |||||
| <label class="form-label">انتخاب زبان</label> | |||||
| <select | |||||
| v-model="locale" | |||||
| class="form-control" | |||||
| placeholder="انتخاب کنید" | |||||
| > | |||||
| <option | |||||
| key="fa" | |||||
| value="fa" | |||||
| > | |||||
| فارسی | |||||
| </option> | |||||
| <option | |||||
| key="en" | |||||
| value="en" | |||||
| > | |||||
| انگلیسی | |||||
| </option> | |||||
| <option | |||||
| key="ar" | |||||
| value="ar" | |||||
| > | |||||
| عربی | |||||
| </option> | |||||
| </select> | |||||
| </div> | |||||
| </BCol> | |||||
| </BRow> | |||||
| <BRow class="g-3"> | |||||
| <!-- Brand Description --> | |||||
| <BCol lg="12"> | |||||
| <div class="form-group"> | |||||
| <label class="form-label">توضیحات دسته</label> | |||||
| <textarea | |||||
| v-model="description" | |||||
| @input="clearError('description')" | |||||
| type="text" | |||||
| class="form-control" | |||||
| placeholder="توضیحات دسته" | |||||
| :class="{ 'is-invalid': errors.description }" | |||||
| ></textarea> | |||||
| <small v-if="errors.description" class="text-danger"> | |||||
| {{ errors.description }} | |||||
| </small> | |||||
| </div> | |||||
| </BCol> | |||||
| </BRow> | |||||
| <!-- Submit Buttons --> | |||||
| <div | |||||
| class="d-flex justify-content-between gap-2" | |||||
| style="margin-top: 20px" | |||||
| > | |||||
| <button | |||||
| class="btn btn-secondary" | |||||
| @click="step--" | |||||
| > | |||||
| قبلی | |||||
| </button> | |||||
| <button type="submit" class="btn btn-primary" :disabled="loading"> | |||||
| <span | |||||
| v-if="loading" | |||||
| class="spinner-border spinner-border-sm" | |||||
| role="status" | |||||
| aria-hidden="true" | |||||
| ></span> | |||||
| ذخیره | |||||
| </button> | |||||
| </div> | </div> | ||||
| </BCol> | |||||
| </BRow> | |||||
| <!-- Submit Buttons --> | |||||
| <div | |||||
| class="d-flex justify-content-end gap-2" | |||||
| style="margin-top: 20px" | |||||
| > | |||||
| <button | |||||
| type="button" | |||||
| class="btn btn-secondary" | |||||
| data-bs-dismiss="modal" | |||||
| id="close" | |||||
| > | |||||
| بستن | |||||
| </button> | |||||
| <button type="submit" class="btn btn-primary" :disabled="loading"> | |||||
| <span | |||||
| v-if="loading" | |||||
| class="spinner-border spinner-border-sm" | |||||
| role="status" | |||||
| aria-hidden="true" | |||||
| ></span> | |||||
| ذخیره | |||||
| </button> | |||||
| </div> | |||||
| </form> | |||||
| </form> | |||||
| </template> | |||||
| </Steppy> | |||||
| </div> | </div> | ||||
| </div> | </div> | ||||
| </div> | </div> | ||||
| @@ -188,12 +181,14 @@ | |||||
| <script> | <script> | ||||
| import { iconData } from "../../../views/live-preview/icon/data"; | import { iconData } from "../../../views/live-preview/icon/data"; | ||||
| import { ref, toRef, watch } from "vue"; | |||||
| import { ref, toRef, watch} from "vue"; | |||||
| import { toast } from "vue3-toastify"; | import { toast } from "vue3-toastify"; | ||||
| import "vue3-toastify/dist/index.css"; | import "vue3-toastify/dist/index.css"; | ||||
| import ApiServiece from "@/services/ApiService"; | import ApiServiece from "@/services/ApiService"; | ||||
| import { Steppy } from "vue3-steppy" | |||||
| export default { | export default { | ||||
| components: { Steppy }, | |||||
| props: { | props: { | ||||
| parents: { | parents: { | ||||
| type: Array, | type: Array, | ||||
| @@ -205,56 +200,21 @@ export default { | |||||
| const selectedPaernt = ref(); | const selectedPaernt = ref(); | ||||
| const localParents = toRef(props.parents); | const localParents = toRef(props.parents); | ||||
| const image = ref(null); | const image = ref(null); | ||||
| const imagePreview = ref(null); | |||||
| const description = ref(); | const description = ref(); | ||||
| const title = ref(); | const title = ref(); | ||||
| const errors = ref({}); | const errors = ref({}); | ||||
| const loading = ref(false); | const loading = ref(false); | ||||
| const imageFileRef = ref(null); | |||||
| const loadingStep = ref(false); | |||||
| const locale = ref('fa'); | |||||
| const step = ref(1); | |||||
| const categoryId = ref(null); | |||||
| watch( | watch( | ||||
| () => props.parents, | () => props.parents, | ||||
| (newVal) => (localParents.value = newVal) | |||||
| ); | |||||
| const removeImage = () => { | |||||
| image.value = null; | |||||
| imagePreview.value = null; | |||||
| const fileInput = document.querySelector('input[type="file"]'); | |||||
| if (fileInput) { | |||||
| fileInput.value = ""; | |||||
| } | |||||
| console.log(image.value); | |||||
| }; | |||||
| const handleImageChange = (event) => { | |||||
| const file = event.target.files[0]; | |||||
| if (file) { | |||||
| if (!file.type.startsWith("image/")) { | |||||
| errors.value.image = "لطفا یک فایل تصویری انتخاب کنید"; | |||||
| imagePreview.value = null; | |||||
| return; | |||||
| } | |||||
| if (file.size > 5 * 1024 * 1024) { | |||||
| errors.value.image = "حجم تصویر باید کمتر از 5 مگابایت باشد"; | |||||
| imagePreview.value = null; | |||||
| return; | |||||
| } | |||||
| errors.value.image = null; | |||||
| image.value = file; | |||||
| const reader = new FileReader(); | |||||
| reader.onload = () => { | |||||
| imagePreview.value = reader.result; | |||||
| }; | |||||
| reader.readAsDataURL(file); | |||||
| (newVal) => { | |||||
| localParents.value = newVal | |||||
| } | } | ||||
| }; | |||||
| ); | |||||
| const setSelectedIcon = (icon) => { | const setSelectedIcon = (icon) => { | ||||
| selectedIcon.value = icon; | selectedIcon.value = icon; | ||||
| @@ -265,8 +225,6 @@ export default { | |||||
| if (!description.value) | if (!description.value) | ||||
| errors.value.description = "وارد کردن توضیحات ضروری می باشد"; | errors.value.description = "وارد کردن توضیحات ضروری می باشد"; | ||||
| if (!title.value) errors.value.title = "وارد کردن عنوان ضروری می باشد"; | if (!title.value) errors.value.title = "وارد کردن عنوان ضروری می باشد"; | ||||
| if (!image.value) errors.value.image = "یک عکس انتخاب نمایید"; | |||||
| if (!selectedIcon.value) errors.value.icon = "انتخاب آیکن ضروری است"; | |||||
| return Object.keys(errors.value).length === 0; | return Object.keys(errors.value).length === 0; | ||||
| }; | }; | ||||
| @@ -285,39 +243,36 @@ export default { | |||||
| loading.value = true; | loading.value = true; | ||||
| const formData = new FormData(); | const formData = new FormData(); | ||||
| formData.append("title", title.value); | formData.append("title", title.value); | ||||
| formData.append("description", description.value); | formData.append("description", description.value); | ||||
| formData.append("image", image.value); | |||||
| formData.append("icon", selectedIcon.value); | |||||
| formData.append("parent_id", selectedPaernt.value ?? ''); | |||||
| ApiServiece.post(`admin/categories`, formData, { | |||||
| formData.append("locale", locale.value); | |||||
| ApiServiece.post(`admin/categories/${categoryId.value}/translations`, formData, { | |||||
| headers: { | headers: { | ||||
| "content-type": "multipart", | "content-type": "multipart", | ||||
| Authorization: `Bearer ${localStorage.getItem("token")}`, | Authorization: `Bearer ${localStorage.getItem("token")}`, | ||||
| }, | }, | ||||
| }) | }) | ||||
| .then((resp) => { | .then((resp) => { | ||||
| console.log(resp); | |||||
| toast.success("!دسته با موفقیت اضافه شد", { | |||||
| console.log(resp,'resp resp') | |||||
| toast.success(resp?.data?.message, { | |||||
| position: "top-right", | position: "top-right", | ||||
| autoClose: 1000, | autoClose: 1000, | ||||
| }); | }); | ||||
| }) | }) | ||||
| .then(() => { | .then(() => { | ||||
| setTimeout(() => { | setTimeout(() => { | ||||
| document.getElementById("close").click(); | |||||
| //document.getElementById("close").click(); | |||||
| emit("cat-updated"); | emit("cat-updated"); | ||||
| title.value = ""; | title.value = ""; | ||||
| imagePreview.value = null; | |||||
| image.value = null; | |||||
| description.value = ""; | description.value = ""; | ||||
| selectedPaernt.value = "" | selectedPaernt.value = "" | ||||
| selectedIcon.value = ""; | |||||
| imageFileRef.value.value = null | |||||
| }, 500); | }, 500); | ||||
| }) | }) | ||||
| .catch((error) => { | .catch((error) => { | ||||
| console.error(error); | |||||
| toast.error(`${error.response.data.message}`, { | toast.error(`${error.response.data.message}`, { | ||||
| position: "top-right", | position: "top-right", | ||||
| autoClose: 1000, | autoClose: 1000, | ||||
| @@ -328,15 +283,45 @@ export default { | |||||
| }); | }); | ||||
| }; | }; | ||||
| const handlerSubmitCategory = async () => { | |||||
| try { | |||||
| loadingStep.value = true; | |||||
| const { data: { success, data, message } } = await ApiServiece.post(`admin/categories`, { | |||||
| parent_id: selectedPaernt.value | |||||
| }, { | |||||
| headers: { | |||||
| Authorization: `Bearer ${localStorage.getItem("token")}`, | |||||
| }, | |||||
| }); | |||||
| if(success) { | |||||
| step.value++ | |||||
| categoryId.value = data?.id | |||||
| toast.success(message) | |||||
| } | |||||
| } catch (e) { | |||||
| console.log(e) | |||||
| } finally { | |||||
| loadingStep.value = false; | |||||
| } | |||||
| }; | |||||
| // onMounted(async () => { | |||||
| // const response = await ApiServiece.get( | |||||
| // `admin/categories/parents` | |||||
| // ); | |||||
| // console.log(response); | |||||
| // }) | |||||
| return { | return { | ||||
| errors, | errors, | ||||
| loading, | loading, | ||||
| clearError, | clearError, | ||||
| addCat, | addCat, | ||||
| handleImageChange, | |||||
| image, | image, | ||||
| imagePreview, | |||||
| removeImage, | |||||
| title, | title, | ||||
| description, | description, | ||||
| localParents, | localParents, | ||||
| @@ -344,13 +329,16 @@ export default { | |||||
| setSelectedIcon, | setSelectedIcon, | ||||
| iconData, | iconData, | ||||
| selectedIcon, | selectedIcon, | ||||
| imageFileRef, | |||||
| locale, | |||||
| step, | |||||
| loadingStep, | |||||
| handlerSubmitCategory | |||||
| }; | }; | ||||
| }, | }, | ||||
| }; | }; | ||||
| </script> | </script> | ||||
| <style scoped> | |||||
| <style> | |||||
| .modal-dialog { | .modal-dialog { | ||||
| max-width: 50%; | max-width: 50%; | ||||
| } | } | ||||
| @@ -455,4 +443,5 @@ export default { | |||||
| align-items: center; | align-items: center; | ||||
| margin: 8px; | margin: 8px; | ||||
| } | } | ||||
| </style> | </style> | ||||
| @@ -1,184 +1,166 @@ | |||||
| <template> | <template> | ||||
| <div | <div | ||||
| class="modal fade" | |||||
| id="editCat" | |||||
| tabindex="-1" | |||||
| role="dialog" | |||||
| aria-labelledby="exampleModalLabel" | |||||
| aria-hidden="true" | |||||
| class="modal fade" | |||||
| id="editCat" | |||||
| tabindex="-1" | |||||
| role="dialog" | |||||
| aria-labelledby="exampleModalLabel" | |||||
| aria-hidden="true" | |||||
| > | > | ||||
| <div class="modal-dialog modal-sm" role="document"> | <div class="modal-dialog modal-sm" role="document"> | ||||
| <div class="modal-content"> | <div class="modal-content"> | ||||
| <div class="modal-header"> | <div class="modal-header"> | ||||
| <h5 class="modal-title" id="exampleModalLabel">ویرایش دسته</h5> | |||||
| <h5 class="modal-title" id="exampleModalLabel"> | |||||
| ویرایش | |||||
| </h5> | |||||
| <button | <button | ||||
| type="button" | |||||
| class="btn-close" | |||||
| data-bs-dismiss="modal" | |||||
| aria-label="Close" | |||||
| type="button" | |||||
| class="btn-close" | |||||
| data-bs-dismiss="modal" | |||||
| aria-label="Close" | |||||
| ></button> | ></button> | ||||
| </div> | </div> | ||||
| <div class="modal-body"> | <div class="modal-body"> | ||||
| <form @submit.prevent="editCat"> | |||||
| <BRow class="g-3"> | |||||
| <!-- Brand Title --> | |||||
| <BCol lg="6"> | |||||
| <div class="form-group"> | |||||
| <label class="form-label">عنوان دسته</label> | |||||
| <input | |||||
| v-model="localTitle" | |||||
| @input="clearError('localTitle')" | |||||
| type="text" | |||||
| class="form-control" | |||||
| placeholder="عنوان دسته" | |||||
| :class="{ 'is-invalid': errors.localTitle }" | |||||
| /> | |||||
| <small v-if="errors.localTitle" class="text-danger"> | |||||
| {{ errors.localTitle }} | |||||
| </small> | |||||
| </div> | |||||
| </BCol> | |||||
| <!-- Brand Image Upload --> | |||||
| <BCol lg="6"> | |||||
| <div class="form-group"> | |||||
| <label class="form-label">تصویر دسته</label> | |||||
| <input | |||||
| ref="imageFileRef" | |||||
| type="file" | |||||
| @input="clearError('localImage')" | |||||
| accept="image/*" | |||||
| @change="handleImageChange" | |||||
| class="form-control" | |||||
| :class="{ 'is-invalid': errors.localImage }" | |||||
| /> | |||||
| <div v-if="imagePreview" class="mt-2"> | |||||
| <img | |||||
| :src="imagePreview" | |||||
| alt="Image Preview" | |||||
| class="img-fluid rounded shadow-sm Image-Preview" | |||||
| /> | |||||
| <BTabs> | |||||
| <BTab title="دسته بندی"> | |||||
| <BRow> | |||||
| <BCol lg="12"> | |||||
| <div class="form-group mt-3"> | |||||
| <label class="form-label">انتخاب پدر</label> | |||||
| <select | |||||
| v-model="selectedPaernt" | |||||
| class="form-control" | |||||
| placeholder="انتخاب کنید" | |||||
| > | |||||
| <option :value="''"> | |||||
| ندارد | |||||
| </option> | |||||
| <option | |||||
| v-for="parent in localParents" | |||||
| :key="parent.id" | |||||
| :value="parent.id" | |||||
| > | |||||
| {{ parent?.translation?.title ?? 'بدون نام' }} | |||||
| </option> | |||||
| </select> | |||||
| </div> | </div> | ||||
| <small v-if="errors.localImage" class="text-danger"> | |||||
| {{ errors.localImage }} | |||||
| </small> | |||||
| </div> | |||||
| </BCol> | |||||
| </BRow> | |||||
| <BRow class="g-3"> | |||||
| <!-- Brand Description --> | |||||
| <BCol lg="12"> | |||||
| <div class="form-group"> | |||||
| <label class="form-label">توضیحات دسته</label> | |||||
| <textarea | |||||
| v-model="localDesc" | |||||
| @input="clearError('localDesc')" | |||||
| type="text" | |||||
| class="form-control" | |||||
| placeholder="توضیحات دسته" | |||||
| :class="{ 'is-invalid': errors.localDesc }" | |||||
| ></textarea> | |||||
| <small v-if="errors.localDesc" class="text-danger"> | |||||
| {{ errors.localDesc }} | |||||
| </small> | |||||
| </div> | |||||
| </BCol> | |||||
| </BRow> | |||||
| <BRow class="g-3"> | |||||
| <!-- Brand Description --> | |||||
| <BCol lg="6"> | |||||
| <div class="form-group"> | |||||
| <label class="form-label">انتخاب پدر</label> | |||||
| <select | |||||
| v-model="localParent" | |||||
| class="form-control" | |||||
| placeholder="انتخاب کنید" | |||||
| <button | |||||
| :disabled="loadingStep" | |||||
| @click="handlerSubmitCategory" | |||||
| class="btn rounded btn-primary w-auto" | |||||
| > | > | ||||
| <option :value="''"> | |||||
| ندارد | |||||
| </option> | |||||
| <option | |||||
| v-for="parent in allLocalParents" | |||||
| :key="parent.id" | |||||
| :value="parent.id" | |||||
| > | |||||
| {{ parent.title }} | |||||
| </option> | |||||
| </select> | |||||
| </div> | |||||
| </BCol> | |||||
| <BCol lg="6"> | |||||
| <div class="form-group"> | |||||
| <label class="form-label">انتخاب آیکن</label> | |||||
| <b-dropdown | |||||
| variant="outline-primary" | |||||
| class="w-100" | |||||
| @change="clearError('icon')" | |||||
| > | |||||
| <b-dropdown-item | |||||
| v-for="(icon, index) in iconData" | |||||
| :key="index" | |||||
| :value="icon.component" | |||||
| class="icon-item" | |||||
| @click="setSelectedIcon(icon.component)" | |||||
| > | |||||
| <div class="icon-container"> | |||||
| <i :class="`ph-duotone ${icon.component}`"></i> | |||||
| </div> | |||||
| </b-dropdown-item> | |||||
| </b-dropdown> | |||||
| <div v-if="localIcon" class="mt-2"> | |||||
| <label class="form-label">آیکن انتخاب شده:</label> | |||||
| <div class="selected-icon-container"> | |||||
| <i :class="`ph-duotone ${localIcon}`"></i> | |||||
| <span | |||||
| v-if="loadingStep" | |||||
| class="spinner-border spinner-border-sm" | |||||
| role="status" | |||||
| /> | |||||
| ذخیره | |||||
| </button> | |||||
| </BCol> | |||||
| </BRow> | |||||
| </BTab> | |||||
| <BTab title="ترجمه ها"> | |||||
| <form @submit.prevent="editCat" class="mt-4"> | |||||
| <BButton | |||||
| :disabled="!findLocaleTranslation" | |||||
| :loading="loadingDelete" | |||||
| @click="handlerRemoveTranslation" | |||||
| class="btn btn-sm rounded btn-danger d-block" | |||||
| style="margin-right: auto" | |||||
| > | |||||
| حذف ترجمه {{ findLocaleTranslation }} | |||||
| </BButton> | |||||
| <BRow class="g-3"> | |||||
| <BCol lg="6"> | |||||
| <div class="form-group"> | |||||
| <label class="form-label">عنوان دسته</label> | |||||
| <input | |||||
| v-model="title" | |||||
| @input="clearError('title')" | |||||
| type="text" | |||||
| class="form-control" | |||||
| placeholder="عنوان دسته" | |||||
| :class="{ 'is-invalid': errors.title }" | |||||
| /> | |||||
| <small v-if="errors.title" class="text-danger"> | |||||
| {{ errors.title }} | |||||
| </small> | |||||
| </div> | </div> | ||||
| </div> | |||||
| <div v-if="!localIcon" class="mt-2"> | |||||
| <label class="form-label">آیکن انتخاب شده:</label> | |||||
| <div class="selected-icon-container"> | |||||
| <i :class="`ph-duotone ${localIcon}`"></i> | |||||
| </BCol> | |||||
| <BCol lg="6"> | |||||
| <div class="form-group"> | |||||
| <label class="form-label">انتخاب زبان</label> | |||||
| <select | |||||
| v-model="locale" | |||||
| class="form-control" | |||||
| placeholder="انتخاب کنید" | |||||
| @change="handlerChangeLocale" | |||||
| > | |||||
| <option | |||||
| key="fa" | |||||
| value="fa" | |||||
| > | |||||
| فارسی | |||||
| </option> | |||||
| <option | |||||
| key="en" | |||||
| value="en" | |||||
| > | |||||
| انگلیسی | |||||
| </option> | |||||
| <option | |||||
| key="ar" | |||||
| value="ar" | |||||
| > | |||||
| عربی | |||||
| </option> | |||||
| </select> | |||||
| </div> | </div> | ||||
| </div> | |||||
| <small v-if="errors.icon" class="text-danger"> | |||||
| {{ errors.icon }} | |||||
| </small> | |||||
| </BCol> | |||||
| </BRow> | |||||
| <BRow class="g-3"> | |||||
| <BCol lg="12"> | |||||
| <div class="form-group"> | |||||
| <label class="form-label">توضیحات دسته</label> | |||||
| <textarea | |||||
| v-model="description" | |||||
| @input="clearError('description')" | |||||
| type="text" | |||||
| class="form-control" | |||||
| placeholder="توضیحات دسته" | |||||
| :class="{ 'is-invalid': errors.description }" | |||||
| ></textarea> | |||||
| <small v-if="errors.description" class="text-danger"> | |||||
| {{ errors.description }} | |||||
| </small> | |||||
| </div> | |||||
| </BCol> | |||||
| </BRow> | |||||
| <!-- Submit Buttons --> | |||||
| <div | |||||
| class="d-flex justify-content-between gap-2" | |||||
| style="margin-top: 20px" | |||||
| > | |||||
| <button type="submit" class="btn btn-primary" :disabled="loading"> | |||||
| <span | |||||
| v-if="loading" | |||||
| class="spinner-border spinner-border-sm" | |||||
| role="status" | |||||
| aria-hidden="true" | |||||
| ></span> | |||||
| ذخیره | |||||
| </button> | |||||
| </div> | </div> | ||||
| </BCol> | |||||
| </BRow> | |||||
| <!-- Submit Buttons --> | |||||
| <div | |||||
| class="d-flex justify-content-end gap-2" | |||||
| style="margin-top: 20px" | |||||
| > | |||||
| <button | |||||
| type="button" | |||||
| class="btn btn-secondary" | |||||
| data-bs-dismiss="modal" | |||||
| id="closeEdit" | |||||
| > | |||||
| بستن | |||||
| </button> | |||||
| <button type="submit" class="btn btn-primary" :disabled="loading"> | |||||
| <span | |||||
| v-if="loading" | |||||
| class="spinner-border spinner-border-sm" | |||||
| role="status" | |||||
| aria-hidden="true" | |||||
| ></span> | |||||
| ذخیره | |||||
| </button> | |||||
| </div> | |||||
| </form> | |||||
| </form> | |||||
| </BTab> | |||||
| </BTabs> | |||||
| </div> | </div> | ||||
| </div> | </div> | ||||
| </div> | </div> | ||||
| @@ -186,133 +168,91 @@ | |||||
| </template> | </template> | ||||
| <script> | <script> | ||||
| import { ref, toRef, watch } from "vue"; | |||||
| import ApiServiece from "@/services/ApiService"; | |||||
| import { ref, toRef, watch, computed } from "vue"; | |||||
| import { toast } from "vue3-toastify"; | import { toast } from "vue3-toastify"; | ||||
| import "vue3-toastify/dist/index.css"; | import "vue3-toastify/dist/index.css"; | ||||
| import { iconData } from "../../../views/live-preview/icon/data"; | |||||
| import ApiServiece from "@/services/ApiService"; | |||||
| import { useModal } from '@/utils/useModal'; | |||||
| import {BButton} from "bootstrap-vue-next"; | |||||
| export default { | export default { | ||||
| components: {BButton}, | |||||
| props: { | props: { | ||||
| id: { | |||||
| type: String, | |||||
| required: true, | |||||
| }, | |||||
| title: { | |||||
| type: String, | |||||
| required: true, | |||||
| }, | |||||
| description: { | |||||
| type: String, | |||||
| required: true, | |||||
| }, | |||||
| image: { | |||||
| type: String, | |||||
| required: true, | |||||
| }, | |||||
| parent: { | |||||
| type: String, | |||||
| required: true, | |||||
| }, | |||||
| allParents: { | |||||
| parents: { | |||||
| type: Array, | type: Array, | ||||
| Required: true, | Required: true, | ||||
| }, | }, | ||||
| icon: { | |||||
| type: String, | |||||
| categoryRow: { | |||||
| type: Object, | |||||
| Required: true, | Required: true, | ||||
| }, | }, | ||||
| }, | }, | ||||
| setup(props, { emit }) { | setup(props, { emit }) { | ||||
| const localIcon = toRef(props.icon); | |||||
| const imagePreview = ref(null); | |||||
| const allLocalParents = toRef(props.allParents); | |||||
| const localParent = toRef(props.parent); | |||||
| const localTitle = toRef(props.title); | |||||
| const localDesc = toRef(props.description); | |||||
| const localImage = toRef(props.image); | |||||
| const selectedPaernt = ref(); | |||||
| const localParents = toRef(props.parents); | |||||
| const image = ref(null); | const image = ref(null); | ||||
| const localId = toRef(props.id); | |||||
| const description = ref(); | |||||
| const title = ref(); | |||||
| const errors = ref({}); | const errors = ref({}); | ||||
| const imageFileRef = ref(null); | |||||
| const loading = ref(false); | const loading = ref(false); | ||||
| const handleImageChange = (event) => { | |||||
| const file = event.target.files[0]; | |||||
| if (file) { | |||||
| if (!file.type.startsWith("image/")) { | |||||
| errors.value.image = "لطفا یک فایل تصویری انتخاب کنید"; | |||||
| imagePreview.value = null; | |||||
| return; | |||||
| const loadingStep = ref(false); | |||||
| const loadingDelete = ref(false); | |||||
| const locale = ref('fa'); | |||||
| const categoryId = ref(null); | |||||
| const { isVisible } = useModal('editCat') | |||||
| const categoryRowModel = computed({ | |||||
| get: () => props.categoryRow, | |||||
| set: (newValue) => emit('update:categoryRow', newValue) | |||||
| }); | |||||
| const findLocaleTranslation = computed(() => { | |||||
| const foundTranslation = categoryRowModel.value?.translations?.find( | |||||
| item => item?.locale === locale.value | |||||
| ); | |||||
| if (foundTranslation) { | |||||
| switch (foundTranslation?.locale) { | |||||
| case "en": | |||||
| return "انگلیسی"; | |||||
| case "fa": | |||||
| return "فارسی"; | |||||
| case "ar": | |||||
| return "عربی"; | |||||
| default: | |||||
| return null; | |||||
| } | } | ||||
| errors.value.localImage = null; | |||||
| image.value = file; | |||||
| const reader = new FileReader(); | |||||
| reader.onload = () => { | |||||
| imagePreview.value = reader.result; | |||||
| }; | |||||
| reader.readAsDataURL(file); | |||||
| } | } | ||||
| }; | |||||
| const setSelectedIcon = (icon) => { | |||||
| localIcon.value = icon; | |||||
| }; | |||||
| return null; | |||||
| }); | |||||
| watch( | watch( | ||||
| () => props.title, | |||||
| (newVal) => (localTitle.value = newVal) | |||||
| ); | |||||
| watch( | |||||
| () => props.description, | |||||
| (newVal) => (localDesc.value = newVal) | |||||
| ); | |||||
| watch( | |||||
| () => props.image, | |||||
| (newVal) => { | |||||
| localImage.value = newVal; | |||||
| imagePreview.value = newVal; | |||||
| } | |||||
| ); | |||||
| () => props.parents, | |||||
| (newVal) => { | |||||
| localParents.value = newVal | |||||
| },{ immediate: true,deep: true }) | |||||
| watch( | |||||
| () => props.id, | |||||
| (newVal) => (localId.value = newVal) | |||||
| ); | |||||
| watch(isVisible, (visible) => { | |||||
| if (visible) { | |||||
| console.log(props.categoryRow,'props.categoryRow') | |||||
| selectedPaernt.value = categoryRowModel.value?.parent?.id | |||||
| watch( | |||||
| () => props.parent, | |||||
| (newVal) => { | |||||
| localParent.value = newVal ?? '' | |||||
| },{ immediate: true }); | |||||
| locale.value = categoryRowModel.value?.translation?.locale | |||||
| watch( | |||||
| () => props.allParents, | |||||
| (newVal) => (allLocalParents.value = newVal) | |||||
| ); | |||||
| title.value = categoryRowModel.value?.translation?.title | |||||
| watch( | |||||
| () => props.icon, | |||||
| (newVal) => (localIcon.value = newVal) | |||||
| ); | |||||
| description.value = categoryRowModel.value?.translation?.description | |||||
| } | |||||
| }); | |||||
| const validateForm = () => { | const validateForm = () => { | ||||
| errors.value = {}; | errors.value = {}; | ||||
| if (!localTitle.value) | |||||
| errors.value.localTitle = "وارد کردن عنوان دسته ضروری می باشد"; | |||||
| if (!localDesc.value) | |||||
| errors.value.localDesc = "وارد کردن توضیحات دسته ضروری می باشد"; | |||||
| if (!localImage.value && !imagePreview.value) { | |||||
| errors.value.localImage = "یک عکس انتخاب نمایید"; | |||||
| } | |||||
| if (!localIcon.value) errors.value.icon = "انتخاب آیکن ضروری است"; | |||||
| if (!description.value) | |||||
| errors.value.description = "وارد کردن توضیحات ضروری می باشد"; | |||||
| if (!title.value) errors.value.title = "وارد کردن عنوان ضروری می باشد"; | |||||
| return Object.keys(errors.value).length === 0; | return Object.keys(errors.value).length === 0; | ||||
| }; | }; | ||||
| @@ -320,8 +260,7 @@ export default { | |||||
| errors.value[field] = ""; | errors.value[field] = ""; | ||||
| }; | }; | ||||
| const editCat = () => { | |||||
| console.log(localId.value); | |||||
| const editCat = async () => { | |||||
| if (!validateForm()) { | if (!validateForm()) { | ||||
| toast.error("لطفا فیلد های لازم را وارد نمایید", { | toast.error("لطفا فیلد های لازم را وارد نمایید", { | ||||
| position: "top-right", | position: "top-right", | ||||
| @@ -329,91 +268,156 @@ export default { | |||||
| }); | }); | ||||
| return; | return; | ||||
| } | } | ||||
| loading.value = true; | |||||
| const formData = new FormData(); | |||||
| formData.append("title", localTitle.value); | |||||
| formData.append("description", localDesc.value); | |||||
| formData.append("icon", localIcon.value); | |||||
| if (image.value) { | |||||
| formData.append("image", image.value); | |||||
| } | |||||
| try { | |||||
| loading.value = true; | |||||
| const params = { title: title.value, description: description.value, locale: locale.value }; | |||||
| const existingTranslation = categoryRowModel.value?.translations?.find( | |||||
| t => t.locale === locale.value | |||||
| ); | |||||
| const { data } = existingTranslation | |||||
| ? await ApiServiece.put( | |||||
| `admin/categories/${categoryRowModel.value?.id}/translations/${existingTranslation?.id}`, | |||||
| params, | |||||
| { | |||||
| headers: { | |||||
| Authorization: `Bearer ${localStorage.getItem("token")}`, | |||||
| }, | |||||
| } | |||||
| ) | |||||
| : await ApiServiece.post( | |||||
| `admin/categories/${categoryRowModel.value?.id}/translations`, | |||||
| params, | |||||
| { | |||||
| headers: { | |||||
| Authorization: `Bearer ${localStorage.getItem("token")}`, | |||||
| }, | |||||
| } | |||||
| ); | |||||
| if (data?.success) { | |||||
| const updatedCategory = props.categoryRow; | |||||
| if (existingTranslation) { | |||||
| updatedCategory.translations = data?.data?.translations | |||||
| } else { | |||||
| updatedCategory.translations = [...updatedCategory.translations, data?.data]; | |||||
| } | |||||
| categoryRowModel.value = updatedCategory; | |||||
| toast.success(data?.message) | |||||
| formData.append("parent_id", localParent.value ?? ''); | |||||
| formData.append("_method", "put"); | |||||
| ApiServiece.post(`admin/categories/${localId.value}`, formData, { | |||||
| headers: { | |||||
| "content-type": "multipart", | |||||
| Authorization: `Bearer ${localStorage.getItem("token")}`, | |||||
| }, | |||||
| }) | |||||
| .then(() => { | |||||
| toast.success("!دسته با موفقیت ویرایش شد", { | |||||
| position: "top-right", | |||||
| autoClose: 1000, | |||||
| }); | |||||
| }) | |||||
| .then(() => { | |||||
| setTimeout(() => { | setTimeout(() => { | ||||
| document.getElementById("closeEdit").click(); | |||||
| emit("cat-updated"); | emit("cat-updated"); | ||||
| }, 500); | |||||
| }) | |||||
| .catch((error) => { | |||||
| console.error(error); | |||||
| toast.error(`${error.response.data.message}`, { | |||||
| position: "top-right", | |||||
| autoClose: 1000, | |||||
| }); | |||||
| }) | |||||
| .finally(() => { | |||||
| loading.value = false; | |||||
| }, 500) | |||||
| } | |||||
| } catch (error) { | |||||
| toast.error(`${error?.response?.data?.message}`, { | |||||
| position: "top-right", | |||||
| autoClose: 1000, | |||||
| }); | }); | ||||
| } finally { | |||||
| loading.value = false; | |||||
| } | |||||
| }; | |||||
| const handlerSubmitCategory = async () => { | |||||
| try { | |||||
| loadingStep.value = true; | |||||
| const { data: { success, data, message } } = await ApiServiece.put( | |||||
| `admin/categories/${categoryRowModel.value?.id}`, { | |||||
| parent_id: selectedPaernt.value | |||||
| }, { | |||||
| headers: { | |||||
| Authorization: `Bearer ${localStorage.getItem("token")}`, | |||||
| }, | |||||
| }); | |||||
| if(success) { | |||||
| categoryId.value = data?.id | |||||
| emit("cat-updated") | |||||
| toast.success(message) | |||||
| } | |||||
| } catch (e) { | |||||
| toast.error(e?.response?.data?.message) | |||||
| } finally { | |||||
| loadingStep.value = false; | |||||
| } | |||||
| }; | }; | ||||
| const handlerChangeLocale = (e) => { | |||||
| const findLocale = categoryRowModel.value?.translations?.find(item => item?.locale === e.target.value); | |||||
| if (findLocale) { | |||||
| title.value = findLocale?.title | |||||
| description.value = findLocale?.description | |||||
| } else { | |||||
| title.value = undefined | |||||
| description.value = undefined | |||||
| } | |||||
| } | |||||
| const handlerRemoveTranslation = async () => { | |||||
| const findLocale = categoryRowModel.value?.translations?.find(item => item?.locale === locale.value); | |||||
| if (findLocale) { | |||||
| try { | |||||
| loadingDelete.value = true; | |||||
| const { data: { success, message, data } } = await ApiServiece.delete( | |||||
| `admin/categories/${categoryRowModel.value?.id}/translations/${findLocale?.id}` | |||||
| ) | |||||
| if (success) { | |||||
| const updatedCategory = props.categoryRow | |||||
| updatedCategory.translations = data?.translations | |||||
| categoryRowModel.value = updatedCategory | |||||
| toast.success(message) | |||||
| } | |||||
| } catch (e) { | |||||
| console.log(e) | |||||
| }finally { | |||||
| loadingDelete.value = false | |||||
| } | |||||
| } | |||||
| } | |||||
| return { | return { | ||||
| errors, | errors, | ||||
| loading, | loading, | ||||
| clearError, | clearError, | ||||
| editCat, | editCat, | ||||
| localTitle, | |||||
| localDesc, | |||||
| localImage, | |||||
| handleImageChange, | |||||
| imagePreview, | |||||
| iconData, | |||||
| localParent, | |||||
| allLocalParents, | |||||
| setSelectedIcon, | |||||
| imageFileRef, | |||||
| localIcon, | |||||
| image, | |||||
| title, | |||||
| description, | |||||
| localParents, | |||||
| selectedPaernt, | |||||
| locale, | |||||
| loadingStep, | |||||
| loadingDelete, | |||||
| handlerSubmitCategory, | |||||
| handlerChangeLocale, | |||||
| handlerRemoveTranslation, | |||||
| findLocaleTranslation | |||||
| }; | }; | ||||
| }, | }, | ||||
| }; | }; | ||||
| </script> | </script> | ||||
| <style scoped> | |||||
| .profile-upload-wrapper { | |||||
| display: flex; | |||||
| flex-direction: column; | |||||
| align-items: center; | |||||
| } | |||||
| .profile-upload-icon { | |||||
| font-size: 64px; | |||||
| color: #6c757d; | |||||
| border: 2px dashed #6c757d; | |||||
| border-radius: 50%; | |||||
| width: 120px; | |||||
| height: 120px; | |||||
| display: flex; | |||||
| align-items: center; | |||||
| justify-content: center; | |||||
| margin-bottom: 10px; | |||||
| } | |||||
| <style> | |||||
| .modal-dialog { | .modal-dialog { | ||||
| max-width: 50%; | max-width: 50%; | ||||
| } | } | ||||
| @@ -422,12 +426,9 @@ export default { | |||||
| padding: 20px; | padding: 20px; | ||||
| } | } | ||||
| .modal-header { | |||||
| border-bottom: 1px solid #dee2e6; | |||||
| } | |||||
| .modal-body { | .modal-body { | ||||
| padding: 20px; | padding: 20px; | ||||
| padding: 1rem 1.5rem; | |||||
| } | } | ||||
| .form-group { | .form-group { | ||||
| @@ -437,107 +438,28 @@ export default { | |||||
| .input-group { | .input-group { | ||||
| margin-top: 0.5rem; | margin-top: 0.5rem; | ||||
| } | } | ||||
| .profile-upload-wrapper { | |||||
| display: flex; | |||||
| flex-direction: column; | |||||
| align-items: center; | |||||
| } | |||||
| .profile-upload-btn { | |||||
| width: 50px; | |||||
| height: 50px; | |||||
| display: flex; | |||||
| align-items: center; | |||||
| justify-content: center; | |||||
| cursor: pointer; | |||||
| border: 2px solid #007bff; | |||||
| background-color: #ffffff; | |||||
| box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); | |||||
| } | |||||
| .profile-upload-btn i { | |||||
| font-size: 20px; | |||||
| color: #007bff; | |||||
| } | |||||
| .profile-image-preview { | |||||
| display: flex; | |||||
| justify-content: center; | |||||
| align-items: center; | |||||
| } | |||||
| .profile-placeholder { | |||||
| border: 2px dashed #007bff; | |||||
| } | |||||
| .d-none { | |||||
| display: none; | |||||
| } | |||||
| .profile-upload-wrapper { | |||||
| display: flex; | |||||
| flex-direction: column; | |||||
| align-items: center; | |||||
| } | |||||
| .profile-upload-btn { | |||||
| width: 30px; | |||||
| height: 30px; | |||||
| padding: 0; | |||||
| display: flex; | |||||
| align-items: center; | |||||
| justify-content: center; | |||||
| border: 1px solid #ccc; | |||||
| background-color: #fff; | |||||
| cursor: pointer; | |||||
| } | |||||
| .profile-upload-btn i { | |||||
| font-size: 16px; | |||||
| color: #007bff; | |||||
| } | |||||
| .profile-image-preview img, | |||||
| .profile-placeholder { | |||||
| width: 80px; | |||||
| height: 80px; | |||||
| } | |||||
| .profile-placeholder i { | |||||
| font-size: 40px; | |||||
| } | |||||
| .modal-dialog { | .modal-dialog { | ||||
| max-width: 50%; | max-width: 50%; | ||||
| } | } | ||||
| .modal-content { | .modal-content { | ||||
| padding: 1.5rem; /* Increased padding for better spacing */ | |||||
| padding: 1.5rem; | |||||
| } | } | ||||
| .modal-header { | .modal-header { | ||||
| border-bottom: 1px solid #dee2e6; | border-bottom: 1px solid #dee2e6; | ||||
| padding-bottom: 1rem; /* Added padding-bottom to separate the header from the content */ | |||||
| } | |||||
| .modal-body { | |||||
| padding: 1rem 1.5rem; /* Adjusted padding for a more balanced layout */ | |||||
| padding-bottom: 1rem; | |||||
| } | } | ||||
| .form-group { | .form-group { | ||||
| margin-bottom: 1.5rem; /* Increased margin between form groups */ | |||||
| margin-bottom: 1.5rem; | |||||
| } | } | ||||
| .input-group { | .input-group { | ||||
| margin-top: 0.5rem; | margin-top: 0.5rem; | ||||
| } | } | ||||
| .profile-image-preview:hover .overlay { | |||||
| opacity: 1; | |||||
| } | |||||
| .profile-image-preview:hover img, | |||||
| .profile-image-preview:hover .profile-placeholder { | |||||
| opacity: 0.7; | |||||
| } | |||||
| .Image-Preview { | .Image-Preview { | ||||
| min-width: 100px; | min-width: 100px; | ||||
| max-height: 100px; | max-height: 100px; | ||||
| @@ -573,6 +495,16 @@ export default { | |||||
| .delete-btn:focus { | .delete-btn:focus { | ||||
| outline: none; | outline: none; | ||||
| } | } | ||||
| .icon-item { | |||||
| display: inline-block; | |||||
| width: 33%; | |||||
| padding: 5px; | |||||
| text-align: center; | |||||
| } | |||||
| .icon-container i { | |||||
| font-size: 2rem; | |||||
| margin-right: 10px; | |||||
| } | |||||
| .selected-icon-container { | .selected-icon-container { | ||||
| display: flex; | display: flex; | ||||
| justify-content: center; | justify-content: center; | ||||
| @@ -591,15 +523,30 @@ export default { | |||||
| margin: 8px; | margin: 8px; | ||||
| } | } | ||||
| .icon-container i { | |||||
| font-size: 2rem; | |||||
| margin-right: 10px; | |||||
| .steppy-item-title { | |||||
| white-space: nowrap; | |||||
| } | } | ||||
| .icon-item { | |||||
| display: inline-block; | |||||
| width: 33%; | |||||
| padding: 5px; | |||||
| text-align: center; | |||||
| .steppy-progress-bar { | |||||
| right: 0 | |||||
| } | |||||
| .steppy-pane { | |||||
| text-align: right !important; | |||||
| box-shadow: none !important; | |||||
| padding: 0 !important; | |||||
| } | |||||
| .btn--default-2 { | |||||
| margin-right: auto; | |||||
| margin-left: unset !important; | |||||
| } | |||||
| .controls { | |||||
| display: none !important; | |||||
| } | |||||
| .wrapper-steppy { | |||||
| padding: 10px; | |||||
| } | } | ||||
| </style> | </style> | ||||
| @@ -29,6 +29,7 @@ export const mutations = { | |||||
| state.token = null; | state.token = null; | ||||
| state.isAuthenticated = false; | state.isAuthenticated = false; | ||||
| localStorage.removeItem("token"); | localStorage.removeItem("token"); | ||||
| localStorage.removeItem("user_profile"); | |||||
| }, | }, | ||||
| SET_LOADING(state, status) { | SET_LOADING(state, status) { | ||||
| @@ -39,7 +40,7 @@ export const mutations = { | |||||
| export const actions = { | export const actions = { | ||||
| async loginUser({ commit }, credentials) { | async loginUser({ commit }, credentials) { | ||||
| try { | try { | ||||
| const { data } = await axios.post(`${url}auth/login`, credentials, { | |||||
| const { data } = await axios.post(`${url}auth/admin/login`, credentials, { | |||||
| headers: { | headers: { | ||||
| "Content-Type": "application/json", | "Content-Type": "application/json", | ||||
| }, | }, | ||||
| @@ -57,6 +58,8 @@ export const actions = { | |||||
| commit("SET_TOKEN", data.data.token); | commit("SET_TOKEN", data.data.token); | ||||
| localStorage.setItem("token", data.data.token); | localStorage.setItem("token", data.data.token); | ||||
| localStorage.setItem("user_profile", JSON.stringify(data.data.user)); | |||||
| } else { | } else { | ||||
| throw new Error("شماره موبایل یا رمز عبور اشتباه است"); | throw new Error("شماره موبایل یا رمز عبور اشتباه است"); | ||||
| } | } | ||||
| @@ -91,6 +94,8 @@ export const actions = { | |||||
| if (user?.role === "admin" || user?.role === "operator") { | if (user?.role === "admin" || user?.role === "operator") { | ||||
| localStorage.setItem("token", token); | localStorage.setItem("token", token); | ||||
| localStorage.setItem("user_profile", JSON.stringify(user)); | |||||
| commit("SET_TOKEN", token); | commit("SET_TOKEN", token); | ||||
| commit("SET_USER", user); | commit("SET_USER", user); | ||||
| @@ -0,0 +1,20 @@ | |||||
| // useModal.js | |||||
| import { onMounted, ref } from 'vue'; | |||||
| export function useModal(modalId) { | |||||
| const isVisible = ref(false); | |||||
| onMounted(() => { | |||||
| const modalEl = document.getElementById(modalId); | |||||
| modalEl.addEventListener('show.bs.modal', () => { | |||||
| isVisible.value = true; | |||||
| }); | |||||
| modalEl.addEventListener('hidden.bs.modal', () => { | |||||
| isVisible.value = false; | |||||
| }); | |||||
| }); | |||||
| return { isVisible }; | |||||
| } | |||||
| @@ -108,7 +108,6 @@ export default { | |||||
| password: password.value, | password: password.value, | ||||
| }); | }); | ||||
| loading.value = false; | loading.value = false; | ||||
| console.log("Login successful"); | |||||
| router.push({ name: "products" }); | router.push({ name: "products" }); | ||||
| } catch (error) { | } catch (error) { | ||||
| console.error("Login failed:", error.message); | console.error("Login failed:", error.message); | ||||
| @@ -702,8 +702,12 @@ export default { | |||||
| } | } | ||||
| formData.append("location", selectedLoc.value); | formData.append("location", selectedLoc.value); | ||||
| formData.append("panel", pannel.value); | formData.append("panel", pannel.value); | ||||
| formData.append("page_type", pageType.value); | |||||
| if(pannel.value === 'web') | |||||
| formData.append("page_type", pageType.value); | |||||
| if (image.value) { | if (image.value) { | ||||
| formData.append("image", image.value); | formData.append("image", image.value); | ||||
| } | } | ||||
| @@ -218,7 +218,6 @@ export default { | |||||
| <table class="table table-hover table-bordered m-0" dir="rtl"> | <table class="table table-hover table-bordered m-0" dir="rtl"> | ||||
| <thead class="table-light"> | <thead class="table-light"> | ||||
| <tr> | <tr> | ||||
| <th>آیکن</th> | |||||
| <th>عنوان</th> | <th>عنوان</th> | ||||
| <th>تاریخ ایجاد</th> | <th>تاریخ ایجاد</th> | ||||
| <th>عملیات</th> | <th>عملیات</th> | ||||
| @@ -226,17 +225,13 @@ export default { | |||||
| </thead> | </thead> | ||||
| <tbody> | <tbody> | ||||
| <tr v-for="cat in cats" :key="cat.id"> | <tr v-for="cat in cats" :key="cat.id"> | ||||
| <td class="icon-container"> | |||||
| <i :class="`ph-duotone ${cat.icon}`"></i> | |||||
| </td> | |||||
| <td>{{ cat.title }}</td> | |||||
| <td>{{ cat?.translation?.title }}</td> | |||||
| <td>{{ convertToJalali(cat.created_at) }}</td> | <td>{{ convertToJalali(cat.created_at) }}</td> | ||||
| <td> | <td> | ||||
| <button | <button | ||||
| @click="editModalData(cat?.id, cat?.title, cat?.icon)" | |||||
| @click="editModalData(cat?.id, cat?.translation?.title)" | |||||
| data-bs-toggle="modal" | data-bs-toggle="modal" | ||||
| data-bs-target="#editBlogCat" | data-bs-target="#editBlogCat" | ||||
| class="btn btn-sm btn-outline-warning me-1" | class="btn btn-sm btn-outline-warning me-1" | ||||
| @@ -7,155 +7,227 @@ | |||||
| <h5>ایجاد بلاگ</h5> | <h5>ایجاد بلاگ</h5> | ||||
| </BCardHeader> | </BCardHeader> | ||||
| <BCardBody> | <BCardBody> | ||||
| <BRow class="g-3"> | |||||
| <BCol md="6"> | |||||
| <div class="form-group"> | |||||
| <label class="form-label">عنوان بلاگ</label> | |||||
| <input | |||||
| type="text" | |||||
| v-model="title" | |||||
| class="form-control" | |||||
| placeholder="عنوان بلاگ" | |||||
| :class="{ 'is-invalid': errors.title }" | |||||
| @input="clearError('title')" | |||||
| /> | |||||
| </div> | |||||
| <small v-if="errors.title" class="text-danger"> | |||||
| {{ errors.title }} | |||||
| </small> | |||||
| </BCol> | |||||
| <BCol md="6"> | |||||
| <div class="form-group"> | |||||
| <label class="form-label">کلمه کلیدی</label> | |||||
| <input | |||||
| type="text" | |||||
| v-model="slug" | |||||
| class="form-control" | |||||
| placeholder="کلمه کلیدی بلاگ" | |||||
| :class="{ 'is-invalid': errors.slug }" | |||||
| @input="clearError('slug')" | |||||
| /> | |||||
| </div> | |||||
| <small v-if="errors.slug" class="text-danger"> | |||||
| {{ errors.slug }} | |||||
| </small> | |||||
| </BCol> | |||||
| <BCol md="6"> | |||||
| <div class="form-group"> | |||||
| <label class="form-label">خلاصه</label> | |||||
| <textarea | |||||
| v-model="summary" | |||||
| class="form-control" | |||||
| placeholder="خلاصه ای از بلاگ" | |||||
| :class="{ 'is-invalid': errors.summary }" | |||||
| @input="clearError('summary')" | |||||
| /> | |||||
| </div> | |||||
| <small v-if="errors.summary" class="text-danger"> | |||||
| {{ errors.summary }} | |||||
| </small> | |||||
| </BCol> | |||||
| <BCol md="6"> | |||||
| <div class="form-group"> | |||||
| <label class="form-label">تصویر بلاگ</label> | |||||
| <input | |||||
| type="file" | |||||
| accept="image/*" | |||||
| @change="handleImageChange" | |||||
| class="form-control" | |||||
| :class="{ 'is-invalid': errors.image }" | |||||
| /> | |||||
| <div v-if="imagePreview" class="mt-2"> | |||||
| <img | |||||
| :src="imagePreview" | |||||
| alt="Image Preview" | |||||
| class="img-fluid rounded shadow-sm Image-Preview" | |||||
| /> | |||||
| <Steppy | |||||
| v-model:step="step" | |||||
| :tabs="[ | |||||
| { title: 'انتخاب دسته بندی', isValid: true }, | |||||
| { title: 'ترجمه ها', isValid: true }, | |||||
| ]" | |||||
| backText="قبلی" | |||||
| nextText="بعدی" | |||||
| doneText="ذخیره" | |||||
| primaryColor1="#04A9F5" | |||||
| circleSize="45" | |||||
| :finalize="submitForm" | |||||
| > | |||||
| <template #1> | |||||
| <BRow class="g-3"> | |||||
| <BCol md="6"> | |||||
| <div class="form-group"> | |||||
| <label class="form-label">تصویر بلاگ</label> | |||||
| <input | |||||
| type="file" | |||||
| accept="image/*" | |||||
| @change="handleImageChange" | |||||
| class="form-control" | |||||
| :class="{ 'is-invalid': errors.image }" | |||||
| /> | |||||
| <div v-if="imagePreview" class="mt-2"> | |||||
| <img | |||||
| :src="imagePreview" | |||||
| alt="Image Preview" | |||||
| class="img-fluid rounded shadow-sm Image-Preview" | |||||
| /> | |||||
| <button | |||||
| type="button" | |||||
| @click="removeImage()" | |||||
| class="delete-btn" | |||||
| > | |||||
| <i class="fa fa-trash f-16"></i> | |||||
| </button> | |||||
| </div> | |||||
| <small v-if="errors.image" class="text-danger"> | |||||
| {{ errors.image }} | |||||
| </small> | |||||
| </div> | |||||
| </BCol> | |||||
| <BCol md="6"> | |||||
| <div class="form-group"> | |||||
| <label class="form-label">دسته</label> | |||||
| <VueSelect | |||||
| :isLoading="categorySelectorLoader" | |||||
| v-model="blogCat" | |||||
| :options="formattedCategories" | |||||
| placeholder="دسته ای را انتخاب کنید" | |||||
| @search="handleSearch" | |||||
| @change="clearError('blogCat')" | |||||
| style="--vs-min-height: 48px; --vs-border-radius: 8px" | |||||
| /> | |||||
| </div> | |||||
| <small v-if="errors.blogCat" class="text-danger"> | |||||
| {{ errors.blogCat }} | |||||
| </small> | |||||
| </BCol> | |||||
| <button | <button | ||||
| type="button" | |||||
| @click="removeImage()" | |||||
| class="delete-btn" | |||||
| :disabled="loadingStep" | |||||
| @click="handlerAddCategory" | |||||
| class="btn rounded btn-primary w-auto" | |||||
| > | > | ||||
| <i class="fa fa-trash f-16"></i> | |||||
| <span | |||||
| v-if="loadingStep" | |||||
| class="spinner-border spinner-border-sm" | |||||
| role="status" | |||||
| /> | |||||
| ذخیره | |||||
| </button> | </button> | ||||
| </div> | |||||
| <small v-if="errors.image" class="text-danger"> | |||||
| {{ errors.image }} | |||||
| </small> | |||||
| </div> | |||||
| </BCol> | |||||
| <BCol md="6"> | |||||
| <div class="form-group"> | |||||
| <label class="form-label">دسته</label> | |||||
| <VueSelect | |||||
| :isLoading="categorySelectorLoader" | |||||
| v-model="blogCat" | |||||
| :options="formattedCategories" | |||||
| placeholder="دسته ای را انتخاب کنید" | |||||
| @search="handleSearch" | |||||
| @change="clearError('blogCat')" | |||||
| style="--vs-min-height: 48px; --vs-border-radius: 8px" | |||||
| /> | |||||
| </div> | |||||
| <small v-if="errors.blogCat" class="text-danger"> | |||||
| {{ errors.blogCat }} | |||||
| </small> | |||||
| </BCol> | |||||
| <BCol md="6"> | |||||
| <div class="form-group"> | |||||
| <label class="form-label">نویسنده</label> | |||||
| <input | |||||
| type="text" | |||||
| v-model="author" | |||||
| class="form-control" | |||||
| placeholder="نویسنده" | |||||
| :class="{ 'is-invalid': errors.author }" | |||||
| @input="clearError('author')" | |||||
| /> | |||||
| </div> | |||||
| <small v-if="errors.author" class="text-danger"> | |||||
| {{ errors.author }} | |||||
| </small> | |||||
| </BCol> | |||||
| <BCol md="12"> | |||||
| <div class="form-group"> | |||||
| <label class="form-label">محتوا</label> | |||||
| <div | |||||
| @input="clearError('editorContent')" | |||||
| ref="editor" | |||||
| class="quill-editor" | |||||
| ></div> | |||||
| </div> | |||||
| <small v-if="errors.editorContent" class="text-danger"> | |||||
| {{ errors.editorContent }} | |||||
| </small> | |||||
| </BCol> | |||||
| </BRow> | |||||
| </BRow> | |||||
| </template> | |||||
| <template #2> | |||||
| <form @submit.prevent="submitForm" class="mt-4"> | |||||
| <BRow class="g-3"> | |||||
| <BCol> | |||||
| <div class="form-group"> | |||||
| <label class="form-label">انتخاب زبان</label> | |||||
| <select | |||||
| v-model="locale" | |||||
| class="form-control" | |||||
| placeholder="انتخاب کنید" | |||||
| > | |||||
| <option | |||||
| key="fa" | |||||
| value="fa" | |||||
| > | |||||
| فارسی | |||||
| </option> | |||||
| <option | |||||
| key="en" | |||||
| value="en" | |||||
| > | |||||
| انگلیسی | |||||
| </option> | |||||
| <option | |||||
| key="ar" | |||||
| value="ar" | |||||
| > | |||||
| عربی | |||||
| </option> | |||||
| </select> | |||||
| </div> | |||||
| </BCol> | |||||
| <BCol md="6"> | |||||
| <div class="form-group"> | |||||
| <label class="form-label">عنوان بلاگ</label> | |||||
| <input | |||||
| type="text" | |||||
| v-model="title" | |||||
| class="form-control" | |||||
| placeholder="عنوان بلاگ" | |||||
| :class="{ 'is-invalid': errors.title }" | |||||
| @input="clearError('title')" | |||||
| /> | |||||
| </div> | |||||
| <small v-if="errors.title" class="text-danger"> | |||||
| {{ errors.title }} | |||||
| </small> | |||||
| </BCol> | |||||
| <BCol md="6"> | |||||
| <div class="form-group"> | |||||
| <label class="form-label">کلمه کلیدی</label> | |||||
| <input | |||||
| type="text" | |||||
| v-model="slug" | |||||
| class="form-control" | |||||
| placeholder="کلمه کلیدی بلاگ" | |||||
| :class="{ 'is-invalid': errors.slug }" | |||||
| @input="clearError('slug')" | |||||
| /> | |||||
| </div> | |||||
| <small v-if="errors.slug" class="text-danger"> | |||||
| {{ errors.slug }} | |||||
| </small> | |||||
| </BCol> | |||||
| <BCol md="6"> | |||||
| <div class="form-group"> | |||||
| <label class="form-label">خلاصه</label> | |||||
| <textarea | |||||
| v-model="summary" | |||||
| class="form-control" | |||||
| placeholder="خلاصه ای از بلاگ" | |||||
| :class="{ 'is-invalid': errors.summary }" | |||||
| @input="clearError('summary')" | |||||
| /> | |||||
| </div> | |||||
| <small v-if="errors.summary" class="text-danger"> | |||||
| {{ errors.summary }} | |||||
| </small> | |||||
| </BCol> | |||||
| <BCol md="6"> | |||||
| <div class="form-group"> | |||||
| <label class="form-label">نویسنده</label> | |||||
| <input | |||||
| type="text" | |||||
| v-model="author" | |||||
| class="form-control" | |||||
| placeholder="نویسنده" | |||||
| :class="{ 'is-invalid': errors.author }" | |||||
| @input="clearError('author')" | |||||
| /> | |||||
| </div> | |||||
| <small v-if="errors.author" class="text-danger"> | |||||
| {{ errors.author }} | |||||
| </small> | |||||
| </BCol> | |||||
| <BCol md="12"> | |||||
| <div class="form-group"> | |||||
| <label class="form-label">محتوا</label> | |||||
| <div | |||||
| @input="clearError('editorContent')" | |||||
| ref="editor" | |||||
| class="quill-editor" | |||||
| ></div> | |||||
| </div> | |||||
| <small v-if="errors.editorContent" class="text-danger"> | |||||
| {{ errors.editorContent }} | |||||
| </small> | |||||
| </BCol> | |||||
| <!-- Submit Buttons --> | |||||
| <div | |||||
| class="d-flex justify-content-between gap-2" | |||||
| style="margin-top: 20px" | |||||
| > | |||||
| <button | |||||
| class="btn btn-secondary" | |||||
| @click="step--" | |||||
| > | |||||
| قبلی | |||||
| </button> | |||||
| <button type="submit" class="btn btn-primary" :disabled="loading"> | |||||
| <span | |||||
| v-if="loading" | |||||
| class="spinner-border spinner-border-sm" | |||||
| role="status" | |||||
| aria-hidden="true" | |||||
| ></span> | |||||
| ذخیره | |||||
| </button> | |||||
| </div> | |||||
| </BRow> | |||||
| </form> | |||||
| </template> | |||||
| </Steppy> | |||||
| </BCardBody> | </BCardBody> | ||||
| <BCardFooter> | |||||
| <div class="d-flex justify-content-center"> | |||||
| <button | |||||
| type="submit" | |||||
| class="btn btn-primary" | |||||
| @click.prevent="submitForm" | |||||
| :disabled="loading" | |||||
| > | |||||
| <span v-if="loading"> | |||||
| <i class="fa fa-spinner fa-spin"></i> بارگذاری... | |||||
| </span> | |||||
| <span v-else>ایجاد</span> | |||||
| </button> | |||||
| </div> | |||||
| </BCardFooter> | |||||
| </BCard> | </BCard> | ||||
| </BCol> | </BCol> | ||||
| </BRow> | </BRow> | ||||
| @@ -167,19 +239,24 @@ import VueSelect from "vue3-select-component"; | |||||
| import { toast } from "vue3-toastify"; | import { toast } from "vue3-toastify"; | ||||
| import "vue3-toastify/dist/index.css"; | import "vue3-toastify/dist/index.css"; | ||||
| import ApiServiece from "@/services/ApiService"; | import ApiServiece from "@/services/ApiService"; | ||||
| import { ref, onMounted, nextTick , computed } from "vue"; | |||||
| import { ref, nextTick , computed } from "vue"; | |||||
| import Layout from "@/layout/custom.vue"; | import Layout from "@/layout/custom.vue"; | ||||
| import Quill from "quill"; | import Quill from "quill"; | ||||
| import "quill/dist/quill.snow.css"; | import "quill/dist/quill.snow.css"; | ||||
| import {Steppy} from "vue3-steppy"; | |||||
| import {BRow} from "bootstrap-vue-next"; | |||||
| export default { | export default { | ||||
| name: "SAMPLE-PAGE", | name: "SAMPLE-PAGE", | ||||
| components: { | components: { | ||||
| BRow, | |||||
| Steppy, | |||||
| Layout, | Layout, | ||||
| VueSelect, | VueSelect, | ||||
| }, | }, | ||||
| setup() { | setup() { | ||||
| const loading = ref(false); | const loading = ref(false); | ||||
| const loadingStep = ref(false); | |||||
| const image = ref(); | const image = ref(); | ||||
| const imagePreview = ref(); | const imagePreview = ref(); | ||||
| const errors = ref({}); | const errors = ref({}); | ||||
| @@ -192,9 +269,9 @@ export default { | |||||
| const categories = ref([]); | const categories = ref([]); | ||||
| const editorContent = ref(""); | const editorContent = ref(""); | ||||
| const categorySelectorLoader = ref(false) | const categorySelectorLoader = ref(false) | ||||
| const step = ref(1) | |||||
| const blogCategoryId = ref(null) | |||||
| const locale = ref('fa') | |||||
| const handleSearch = async (searchTerm) => { | const handleSearch = async (searchTerm) => { | ||||
| if (searchTerm.length < 3) return; | if (searchTerm.length < 3) return; | ||||
| @@ -214,8 +291,8 @@ export default { | |||||
| const formattedCategories = computed(() => | const formattedCategories = computed(() => | ||||
| Array.isArray(categories.value) | Array.isArray(categories.value) | ||||
| ? categories.value.map((category) => ({ | ? categories.value.map((category) => ({ | ||||
| value: category.id, | |||||
| label: category.title, | |||||
| value: category?.translation?.id, | |||||
| label: category?.translation?.title, | |||||
| })) | })) | ||||
| : [] | : [] | ||||
| ); | ); | ||||
| @@ -253,13 +330,10 @@ export default { | |||||
| errors.value.slug = "وارد کردن کلمه کلیدی بلاگ ضروری می باشد"; | errors.value.slug = "وارد کردن کلمه کلیدی بلاگ ضروری می باشد"; | ||||
| if (!summary.value) | if (!summary.value) | ||||
| errors.value.summary = "وارد کردن خلاصه بلاگ ضروری می باشد"; | errors.value.summary = "وارد کردن خلاصه بلاگ ضروری می باشد"; | ||||
| if (!blogCat.value) | |||||
| errors.value.blogCat = "انتخاب دسته برای بلاگ ضروری می باشد"; | |||||
| if (!author.value) | if (!author.value) | ||||
| errors.value.author = "وارد کردن نویسنده بلاگ ضروری می باشد"; | errors.value.author = "وارد کردن نویسنده بلاگ ضروری می باشد"; | ||||
| if (!editorContent.value) | if (!editorContent.value) | ||||
| errors.value.editorContent = "وارد کردن محتوای بلاگ ضروری می باشد"; | errors.value.editorContent = "وارد کردن محتوای بلاگ ضروری می باشد"; | ||||
| if (!image.value) errors.value.image = "وارد کردن عکس بلاگ ضروری می باشد"; | |||||
| return Object.keys(errors.value).length === 0; | return Object.keys(errors.value).length === 0; | ||||
| }; | }; | ||||
| @@ -267,39 +341,6 @@ export default { | |||||
| errors.value[field] = ""; | errors.value[field] = ""; | ||||
| }; | }; | ||||
| onMounted(() => { | |||||
| const quill = new Quill(editor.value, { | |||||
| theme: "snow", | |||||
| modules: { | |||||
| toolbar: [ | |||||
| [{ header: "1" }, { header: "2" }, { font: [] }], | |||||
| [{ list: "ordered" }, { list: "bullet" }], | |||||
| [{ align: [] }], | |||||
| ["bold", "italic", "underline"], | |||||
| ["link", "image"], | |||||
| [{ script: "sub" }, { script: "super" }], | |||||
| [{ direction: "rtl" }], | |||||
| ], | |||||
| }, | |||||
| }); | |||||
| quill.root.setAttribute("dir", "rtl"); | |||||
| quill.format("direction", "rtl"); | |||||
| nextTick(() => { | |||||
| const rtlButton = quill.container.querySelector( | |||||
| ".ql-direction[data-value='rtl']" | |||||
| ); | |||||
| if (rtlButton) { | |||||
| rtlButton.click(); | |||||
| } | |||||
| }); | |||||
| quill.on("text-change", () => { | |||||
| editorContent.value = quill.root.innerHTML; | |||||
| }); | |||||
| }); | |||||
| const submitForm = () => { | const submitForm = () => { | ||||
| if (!validateForm()) { | if (!validateForm()) { | ||||
| toast.error("لطفا فیلد های لازم را وارد نمایید", { | toast.error("لطفا فیلد های لازم را وارد نمایید", { | ||||
| @@ -311,33 +352,34 @@ export default { | |||||
| loading.value = true; | loading.value = true; | ||||
| const formData = new FormData(); | |||||
| formData.append("title", title.value); | |||||
| formData.append("slug", slug.value); | |||||
| formData.append("summary", summary.value); | |||||
| formData.append("content", editorContent.value); | |||||
| formData.append("image", image.value); | |||||
| formData.append("author", author.value); | |||||
| formData.append("blog_category_id", blogCat.value); | |||||
| const params = { | |||||
| title: title.value, | |||||
| slug: slug.value, | |||||
| summary: summary.value, | |||||
| content: editorContent.value, | |||||
| author: author.value, | |||||
| locale: locale.value, | |||||
| } | |||||
| ApiServiece.post(`admin/blogs`, formData, { | |||||
| ApiServiece.post(`admin/blogs/${blogCategoryId.value}/translations`, params, { | |||||
| headers: { | headers: { | ||||
| "content-type": "multipart", | |||||
| Authorization: `Bearer ${localStorage.getItem("token")}`, | Authorization: `Bearer ${localStorage.getItem("token")}`, | ||||
| }, | }, | ||||
| }) | }) | ||||
| .then(() => { | |||||
| toast.success("!بلاگ با موفقیت اضافه شد", { | |||||
| .then(({ data }) => { | |||||
| toast.success(data?.message, { | |||||
| position: "top-right", | position: "top-right", | ||||
| autoClose: 1000, | autoClose: 1000, | ||||
| }); | }); | ||||
| setTimeout(() => { | |||||
| window.location.reload(); | |||||
| }, 1500); | |||||
| title.value = "" | |||||
| slug.value = "" | |||||
| summary.value = "" | |||||
| editorContent.value = "" | |||||
| author.value = "" | |||||
| locale.value = "fa" | |||||
| }) | }) | ||||
| .catch((error) => { | |||||
| console.error(error); | |||||
| .catch(() => { | |||||
| toast.error("!مشکلی در اضافه کردن بلاگ پیش آمد", { | toast.error("!مشکلی در اضافه کردن بلاگ پیش آمد", { | ||||
| position: "top-right", | position: "top-right", | ||||
| autoClose: 1000, | autoClose: 1000, | ||||
| @@ -348,6 +390,79 @@ export default { | |||||
| }); | }); | ||||
| }; | }; | ||||
| const handlerAddCategory = async () => { | |||||
| try { | |||||
| loadingStep.value = true; | |||||
| const formData = new FormData(); | |||||
| formData.append("image", image.value); | |||||
| formData.append("blog_category_id", blogCat.value); | |||||
| const { data: { message, success, data } } = await ApiServiece.post(`admin/blogs`, formData, { | |||||
| headers: { | |||||
| "content-type": "multipart", | |||||
| Authorization: `Bearer ${localStorage.getItem("token")}`, | |||||
| }, | |||||
| }) | |||||
| if (success) { | |||||
| toast.success(message) | |||||
| initQuill() | |||||
| blogCategoryId.value = data?.id | |||||
| step.value++ | |||||
| } | |||||
| } catch (e) { | |||||
| toast.error(e?.response?.data?.message) | |||||
| } finally { | |||||
| loadingStep.value = false; | |||||
| } | |||||
| } | |||||
| const initQuill = () => { | |||||
| nextTick(() => { | |||||
| if (!editor.value) { | |||||
| console.error("Editor container not found!"); | |||||
| return; | |||||
| } | |||||
| const quill = new Quill(editor.value, { | |||||
| theme: "snow", | |||||
| modules: { | |||||
| toolbar: [ | |||||
| [{ header: "1" }, { header: "2" }, { font: [] }], | |||||
| [{ list: "ordered" }, { list: "bullet" }], | |||||
| [{ align: [] }], | |||||
| ["bold", "italic", "underline"], | |||||
| ["link", "image"], | |||||
| [{ script: "sub" }, { script: "super" }], | |||||
| [{ direction: "rtl" }], | |||||
| ], | |||||
| }, | |||||
| }); | |||||
| quill.root.setAttribute("dir", "rtl"); | |||||
| quill.format("direction", "rtl"); | |||||
| nextTick(() => { | |||||
| const rtlButton = quill.container.querySelector(".ql-direction[data-value='rtl']"); | |||||
| if (rtlButton) { | |||||
| rtlButton.click(); | |||||
| } | |||||
| }); | |||||
| quill.on("text-change", () => { | |||||
| editorContent.value = quill.root.innerHTML; | |||||
| }); | |||||
| }) | |||||
| } | |||||
| return { | return { | ||||
| title, | title, | ||||
| slug, | slug, | ||||
| @@ -365,9 +480,14 @@ export default { | |||||
| author, | author, | ||||
| blogCat, | blogCat, | ||||
| loading, | loading, | ||||
| loadingStep, | |||||
| step, | |||||
| handleSearch, | handleSearch, | ||||
| categorySelectorLoader, | categorySelectorLoader, | ||||
| formattedCategories | |||||
| formattedCategories, | |||||
| handlerAddCategory, | |||||
| blogCategoryId, | |||||
| locale | |||||
| }; | }; | ||||
| }, | }, | ||||
| }; | }; | ||||
| @@ -93,7 +93,7 @@ export default { | |||||
| }); | }); | ||||
| const deleteBlog = (id, title) => { | const deleteBlog = (id, title) => { | ||||
| Swal.fire({ | Swal.fire({ | ||||
| text: `می خواهید بلاگ ${title} را حذف کنید؟`, | |||||
| text: `می خواهید بلاگ ${title ?? ''} را حذف کنید؟`, | |||||
| icon: "warning", | icon: "warning", | ||||
| showCancelButton: true, | showCancelButton: true, | ||||
| confirmButtonColor: "#3085d6", | confirmButtonColor: "#3085d6", | ||||
| @@ -110,8 +110,7 @@ export default { | |||||
| }); | }); | ||||
| blogs.value = blogs.value.filter((blog) => blog.id !== id); | blogs.value = blogs.value.filter((blog) => blog.id !== id); | ||||
| }) | }) | ||||
| .catch((err) => { | |||||
| console.log(err); | |||||
| .catch(() => { | |||||
| toast.error("!مشکلی در حذف کردن بلاگ پیش آمد", { | toast.error("!مشکلی در حذف کردن بلاگ پیش آمد", { | ||||
| position: "top-right", | position: "top-right", | ||||
| autoClose: 3000, | autoClose: 3000, | ||||
| @@ -220,8 +219,8 @@ export default { | |||||
| /> | /> | ||||
| </td> | </td> | ||||
| <td v-if="!blog.image">ندارد</td> | <td v-if="!blog.image">ندارد</td> | ||||
| <td>{{ blog.title }}</td> | |||||
| <td>{{ blog.slug }}</td> | |||||
| <td>{{ blog?.translation?.title }}</td> | |||||
| <td>{{ blog?.translation?.slug }}</td> | |||||
| <td>{{ convertToJalali(blog?.created_at) }}</td> | <td>{{ convertToJalali(blog?.created_at) }}</td> | ||||
| <td> | <td> | ||||
| <router-link | <router-link | ||||
| @@ -231,7 +230,7 @@ export default { | |||||
| ویرایش | ویرایش | ||||
| </router-link> | </router-link> | ||||
| <button | <button | ||||
| @click="deleteBlog(blog?.id, blog?.title)" | |||||
| @click="deleteBlog(blog?.id, blog?.translation?.title)" | |||||
| class="btn btn-sm btn-outline-danger" | class="btn btn-sm btn-outline-danger" | ||||
| > | > | ||||
| حذف | حذف | ||||
| @@ -7,151 +7,207 @@ | |||||
| <h5>ویرایش بلاگ</h5> | <h5>ویرایش بلاگ</h5> | ||||
| </BCardHeader> | </BCardHeader> | ||||
| <BCardBody> | <BCardBody> | ||||
| <BRow class="g-3"> | |||||
| <BCol md="6"> | |||||
| <div class="form-group"> | |||||
| <label class="form-label">عنوان بلاگ</label> | |||||
| <input | |||||
| type="text" | |||||
| v-model="title" | |||||
| class="form-control" | |||||
| placeholder="عنوان بلاگ" | |||||
| :class="{ 'is-invalid': errors.title }" | |||||
| @input="clearError('title')" | |||||
| /> | |||||
| </div> | |||||
| <small v-if="errors.title" class="text-danger"> | |||||
| {{ errors.title }} | |||||
| </small> | |||||
| </BCol> | |||||
| <!-- Second Input Field (Slug) --> | |||||
| <BCol md="6"> | |||||
| <div class="form-group"> | |||||
| <label class="form-label">کلمه کلیدی</label> | |||||
| <input | |||||
| type="text" | |||||
| v-model="slug" | |||||
| class="form-control" | |||||
| placeholder="کلمه کلیدی بلاگ" | |||||
| :class="{ 'is-invalid': errors.slug }" | |||||
| @input="clearError('slug')" | |||||
| /> | |||||
| </div> | |||||
| <small v-if="errors.slug" class="text-danger"> | |||||
| {{ errors.slug }} | |||||
| </small> | |||||
| </BCol> | |||||
| <!-- Summary Textarea --> | |||||
| <BCol md="6"> | |||||
| <div class="form-group"> | |||||
| <label class="form-label">خلاصه</label> | |||||
| <textarea | |||||
| v-model="summary" | |||||
| class="form-control" | |||||
| placeholder="خلاصه ای از بلاگ" | |||||
| :class="{ 'is-invalid': errors.summary }" | |||||
| @input="clearError('summary')" | |||||
| /> | |||||
| </div> | |||||
| <small v-if="errors.summary" class="text-danger"> | |||||
| {{ errors.summary }} | |||||
| </small> | |||||
| </BCol> | |||||
| <BCol md="6"> | |||||
| <div class="form-group"> | |||||
| <label class="form-label">تصویر بلاگ</label> | |||||
| <input | |||||
| type="file" | |||||
| accept="image/*" | |||||
| @change="handleImageChange" | |||||
| class="form-control" | |||||
| :class="{ 'is-invalid': errors.image }" | |||||
| /> | |||||
| <div v-if="imagePreview" class="mt-2"> | |||||
| <img | |||||
| :src="imagePreview" | |||||
| alt="Image Preview" | |||||
| class="img-fluid rounded shadow-sm Image-Preview" | |||||
| <BTabs> | |||||
| <BTab title="عمومی"> | |||||
| <BRow class="mt-4"> | |||||
| <BCol md="6"> | |||||
| <div class="form-group"> | |||||
| <label class="form-label">تصویر بلاگ</label> | |||||
| <input | |||||
| type="file" | |||||
| accept="image/*" | |||||
| @change="handleImageChange" | |||||
| class="form-control" | |||||
| :class="{ 'is-invalid': errors.image }" | |||||
| /> | |||||
| <div v-if="imagePreview" class="mt-2"> | |||||
| <img | |||||
| :src="imagePreview" | |||||
| alt="Image Preview" | |||||
| class="img-fluid rounded shadow-sm Image-Preview" | |||||
| /> | |||||
| </div> | |||||
| <small v-if="errors.image" class="text-danger"> | |||||
| {{ errors.image }} | |||||
| </small> | |||||
| </div> | |||||
| </BCol> | |||||
| <BCol md="6"> | |||||
| <div class="form-group"> | |||||
| <label class="form-label">دسته</label> | |||||
| <VueSelect | |||||
| style="--vs-min-height: 48px; --vs-border-radius: 8px" | |||||
| :isLoading="categorySelectorLoader" | |||||
| v-model="blogCat" | |||||
| :options="formattedCategories" | |||||
| :reduce="(option) => option.value" | |||||
| placeholder="دسته ای را انتخاب نمایید" | |||||
| @search="handleSearch" | |||||
| /> | |||||
| </div> | |||||
| <small v-if="errors.blogCat" class="text-danger"> | |||||
| {{ errors.blogCat }} | |||||
| </small> | |||||
| </BCol> | |||||
| <button | |||||
| :disabled="loadingFirstTab" | |||||
| @click="handlerSubmitCategory" | |||||
| class="btn rounded btn-primary w-auto mt-5 d-flex justify-content-center align-items-center mx-auto" | |||||
| > | |||||
| <span | |||||
| v-if="loadingFirstTab" | |||||
| class="spinner-border spinner-border-sm " | |||||
| role="status" | |||||
| /> | /> | ||||
| </div> | |||||
| <small v-if="errors.image" class="text-danger"> | |||||
| {{ errors.image }} | |||||
| </small> | |||||
| ذخیره | |||||
| </button> | |||||
| </BRow> | |||||
| </BTab> | |||||
| <BTab title="ترجمه ها"> | |||||
| <BButton | |||||
| :disabled="!findLocaleTranslation" | |||||
| :loading="loadingDelete" | |||||
| @click="handlerRemoveTranslation" | |||||
| class="btn btn-sm rounded btn-danger d-block mt-5" | |||||
| style="margin-right: auto" | |||||
| > | |||||
| حذف ترجمه {{ findLocaleTranslation }} | |||||
| </BButton> | |||||
| <BRow class="g-3 mt-4"> | |||||
| <BCol lg="6"> | |||||
| <div class="form-group"> | |||||
| <label class="form-label">انتخاب زبان</label> | |||||
| <select | |||||
| v-model="locale" | |||||
| class="form-control" | |||||
| placeholder="انتخاب کنید" | |||||
| @change="handlerChangeLocale" | |||||
| > | |||||
| <option | |||||
| key="fa" | |||||
| value="fa" | |||||
| > | |||||
| فارسی | |||||
| </option> | |||||
| <option | |||||
| key="en" | |||||
| value="en" | |||||
| > | |||||
| انگلیسی | |||||
| </option> | |||||
| <option | |||||
| key="ar" | |||||
| value="ar" | |||||
| > | |||||
| عربی | |||||
| </option> | |||||
| </select> | |||||
| </div> | |||||
| </BCol> | |||||
| <BCol md="6"> | |||||
| <div class="form-group"> | |||||
| <label class="form-label">عنوان بلاگ</label> | |||||
| <input | |||||
| type="text" | |||||
| v-model="title" | |||||
| class="form-control" | |||||
| placeholder="عنوان بلاگ" | |||||
| :class="{ 'is-invalid': errors.title }" | |||||
| @input="clearError('title')" | |||||
| /> | |||||
| </div> | |||||
| <small v-if="errors.title" class="text-danger"> | |||||
| {{ errors.title }} | |||||
| </small> | |||||
| </BCol> | |||||
| <BCol md="6"> | |||||
| <div class="form-group"> | |||||
| <label class="form-label">کلمه کلیدی</label> | |||||
| <input | |||||
| type="text" | |||||
| v-model="slug" | |||||
| class="form-control" | |||||
| placeholder="کلمه کلیدی بلاگ" | |||||
| :class="{ 'is-invalid': errors.slug }" | |||||
| @input="clearError('slug')" | |||||
| /> | |||||
| </div> | |||||
| <small v-if="errors.slug" class="text-danger"> | |||||
| {{ errors.slug }} | |||||
| </small> | |||||
| </BCol> | |||||
| <BCol md="6"> | |||||
| <div class="form-group"> | |||||
| <label class="form-label">خلاصه</label> | |||||
| <textarea | |||||
| v-model="summary" | |||||
| class="form-control" | |||||
| placeholder="خلاصه ای از بلاگ" | |||||
| :class="{ 'is-invalid': errors.summary }" | |||||
| @input="clearError('summary')" | |||||
| /> | |||||
| </div> | |||||
| <small v-if="errors.summary" class="text-danger"> | |||||
| {{ errors.summary }} | |||||
| </small> | |||||
| </BCol> | |||||
| <BCol md="6"> | |||||
| <div class="form-group"> | |||||
| <label class="form-label">نویسنده</label> | |||||
| <input | |||||
| type="text" | |||||
| v-model="author" | |||||
| class="form-control" | |||||
| placeholder="نویسنده" | |||||
| :class="{ 'is-invalid': errors.author }" | |||||
| @input="clearError('author')" | |||||
| /> | |||||
| </div> | |||||
| <small v-if="errors.author" class="text-danger"> | |||||
| {{ errors.author }} | |||||
| </small> | |||||
| </BCol> | |||||
| <BCol md="12"> | |||||
| <div class="form-group"> | |||||
| <label class="form-label">محتوا</label> | |||||
| <div | |||||
| @input="clearError('editorContent')" | |||||
| ref="editor" | |||||
| class="quill-editor" | |||||
| ></div> | |||||
| </div> | |||||
| <small v-if="errors.editorContent" class="text-danger"> | |||||
| {{ errors.editorContent }} | |||||
| </small> | |||||
| </BCol> | |||||
| </BRow> | |||||
| <div class="d-flex justify-content-center"> | |||||
| <button | |||||
| type="submit" | |||||
| class="btn btn-primary mt-5" | |||||
| @click.prevent="submitForm" | |||||
| :disabled="loading" | |||||
| > | |||||
| <span v-if="loading"> | |||||
| <i class="fa fa-spinner fa-spin"></i> بارگذاری... | |||||
| </span> | |||||
| <span v-else>ویرایش</span> | |||||
| </button> | |||||
| </div> | </div> | ||||
| </BCol> | |||||
| <BCol md="6"> | |||||
| <div class="form-group"> | |||||
| <label class="form-label">دسته</label> | |||||
| <VueSelect | |||||
| style="--vs-min-height: 48px; --vs-border-radius: 8px" | |||||
| :isLoading="categorySelectorLoader" | |||||
| v-model="blogCat" | |||||
| :options="formattedCategories" | |||||
| :reduce="(option) => option.value" | |||||
| placeholder="دسته ای را انتخاب نمایید" | |||||
| @search="handleSearch" | |||||
| /> | |||||
| </div> | |||||
| <small v-if="errors.blogCat" class="text-danger"> | |||||
| {{ errors.blogCat }} | |||||
| </small> | |||||
| </BCol> | |||||
| <BCol md="6"> | |||||
| <div class="form-group"> | |||||
| <label class="form-label">نویسنده</label> | |||||
| <input | |||||
| type="text" | |||||
| v-model="author" | |||||
| class="form-control" | |||||
| placeholder="نویسنده" | |||||
| :class="{ 'is-invalid': errors.author }" | |||||
| @input="clearError('author')" | |||||
| /> | |||||
| </div> | |||||
| <small v-if="errors.author" class="text-danger"> | |||||
| {{ errors.author }} | |||||
| </small> | |||||
| </BCol> | |||||
| <!-- Quill Editor --> | |||||
| <BCol md="12"> | |||||
| <div class="form-group"> | |||||
| <label class="form-label">محتوا</label> | |||||
| <div | |||||
| @input="clearError('editorContent')" | |||||
| ref="editor" | |||||
| class="quill-editor" | |||||
| ></div> | |||||
| </div> | |||||
| <small v-if="errors.editorContent" class="text-danger"> | |||||
| {{ errors.editorContent }} | |||||
| </small> | |||||
| </BCol> | |||||
| </BRow> | |||||
| </BTab> | |||||
| </BTabs> | |||||
| </BCardBody> | </BCardBody> | ||||
| <BCardFooter> | |||||
| <div class="d-flex justify-content-center"> | |||||
| <button | |||||
| type="submit" | |||||
| class="btn btn-primary" | |||||
| @click.prevent="submitForm" | |||||
| :disabled="loading" | |||||
| > | |||||
| <span v-if="loading"> | |||||
| <i class="fa fa-spinner fa-spin"></i> بارگذاری... | |||||
| </span> | |||||
| <span v-else>ویرایش</span> | |||||
| </button> | |||||
| </div> | |||||
| </BCardFooter> | |||||
| </BCard> | </BCard> | ||||
| </BCol> | </BCol> | ||||
| </BRow> | </BRow> | ||||
| @@ -168,9 +224,11 @@ import Layout from "@/layout/custom.vue"; | |||||
| import Quill from "quill"; | import Quill from "quill"; | ||||
| import "quill/dist/quill.snow.css"; | import "quill/dist/quill.snow.css"; | ||||
| import { useRoute } from "vue-router"; | import { useRoute } from "vue-router"; | ||||
| import {BTabs} from "bootstrap-vue-next"; | |||||
| export default { | export default { | ||||
| name: "SAMPLE-PAGE", | name: "SAMPLE-PAGE", | ||||
| components: { | components: { | ||||
| BTabs, | |||||
| Layout, | Layout, | ||||
| VueSelect, | VueSelect, | ||||
| }, | }, | ||||
| @@ -189,26 +247,51 @@ export default { | |||||
| const author = ref(""); | const author = ref(""); | ||||
| const editor = ref(null); | const editor = ref(null); | ||||
| const categorySelectorLoader = ref(false); | const categorySelectorLoader = ref(false); | ||||
| const categories = ref([{ id: null, title: "" }]); | |||||
| const categories = ref([{ value: null, label: null }]); | |||||
| const editorContent = ref(""); | const editorContent = ref(""); | ||||
| const locale = ref("fa"); | |||||
| const blogTranslationId = ref(); | |||||
| const loadingFirstTab = ref(false); | |||||
| const loadingDelete = ref(false); | |||||
| const formattedCategories = computed(() => | const formattedCategories = computed(() => | ||||
| Array.isArray(categories.value) | Array.isArray(categories.value) | ||||
| ? categories.value.map((category) => ({ | ? categories.value.map((category) => ({ | ||||
| value: category.id, | |||||
| label: category.title, | |||||
| value: category?.translation?.id, | |||||
| label: category?.translation?.title, | |||||
| })) | })) | ||||
| : [] | : [] | ||||
| ); | ); | ||||
| const findLocaleTranslation = computed(() => { | |||||
| const foundTranslation = blog.value?.translations?.find( | |||||
| item => item?.locale === locale.value | |||||
| ); | |||||
| if (foundTranslation) { | |||||
| switch (foundTranslation?.locale) { | |||||
| case "en": | |||||
| return "انگلیسی"; | |||||
| case "fa": | |||||
| return "فارسی"; | |||||
| case "ar": | |||||
| return "عربی"; | |||||
| default: | |||||
| return null; | |||||
| } | |||||
| } | |||||
| return null; | |||||
| }); | |||||
| const handleSearch = async (searchTerm) => { | const handleSearch = async (searchTerm) => { | ||||
| if (searchTerm.length < 3) return; | |||||
| if (searchTerm?.length < 3) return; | |||||
| categorySelectorLoader.value = true; | categorySelectorLoader.value = true; | ||||
| try { | try { | ||||
| const response = await ApiServiece.get( | const response = await ApiServiece.get( | ||||
| `admin/blog-categories?title=${searchTerm}` | |||||
| `admin/blog-categories?title=${searchTerm ?? ''}` | |||||
| ); | ); | ||||
| categories.value = response.data.data; | categories.value = response.data.data; | ||||
| categorySelectorLoader.value = false; | categorySelectorLoader.value = false; | ||||
| @@ -249,7 +332,7 @@ export default { | |||||
| errors.value.editorContent = "وارد کردن محتوای بلاگ ضروری می باشد"; | errors.value.editorContent = "وارد کردن محتوای بلاگ ضروری می باشد"; | ||||
| if (!imagePreview.value) | if (!imagePreview.value) | ||||
| errors.value.image = "وارد کردن عکس بلاگ ضروری می باشد"; | errors.value.image = "وارد کردن عکس بلاگ ضروری می باشد"; | ||||
| return Object.keys(errors.value).length === 0; | |||||
| return Object.keys(errors.value)?.length === 0; | |||||
| }; | }; | ||||
| const clearError = (field) => { | const clearError = (field) => { | ||||
| @@ -260,16 +343,25 @@ export default { | |||||
| ApiServiece.get(`admin/blogs/${route.params.id}`) | ApiServiece.get(`admin/blogs/${route.params.id}`) | ||||
| .then((resp) => { | .then((resp) => { | ||||
| blog.value = resp.data.data; | blog.value = resp.data.data; | ||||
| categories.value[0].id = blog.value?.blog_category_id; | |||||
| categories.value[0].title = blog.value?.blog_category?.title; | |||||
| title.value = blog.value.title; | |||||
| slug.value = blog.value.slug; | |||||
| summary.value = blog.value.summary; | |||||
| imagePreview.value = blog.value.image; | |||||
| categories.value[0].value = blog.value?.blog_category_id; | |||||
| categories.value[0].lable = blog.value?.blog_category?.title; | |||||
| title.value = blog.value?.translation?.title; | |||||
| slug.value = blog.value?.translation?.slug; | |||||
| summary.value = blog.value?.translation?.summary; | |||||
| imagePreview.value = blog.value?.image; | |||||
| blogCat.value = blog.value?.blog_category_id; | |||||
| author.value = blog.value?.translation?.author; | |||||
| locale.value = blog.value?.translation?.locale; | |||||
| blogCat.value = blog.value.blog_category_id; | |||||
| author.value = blog.value.author; | |||||
| if (editor.value) { | if (editor.value) { | ||||
| quillInstance.value = new Quill(editor.value, { | quillInstance.value = new Quill(editor.value, { | ||||
| theme: "snow", | theme: "snow", | ||||
| @@ -286,8 +378,9 @@ export default { | |||||
| }, | }, | ||||
| }); | }); | ||||
| quillInstance.value.root.innerHTML = blog.value.content; | |||||
| editorContent.value = blog.value.content; | |||||
| quillInstance.value.root.innerHTML = blog.value?.translation?.content; | |||||
| editorContent.value = blog.value?.translation?.content; | |||||
| // ✨ Update content on change | // ✨ Update content on change | ||||
| quillInstance.value.on("text-change", () => { | quillInstance.value.on("text-change", () => { | ||||
| @@ -300,8 +393,10 @@ export default { | |||||
| }); | }); | ||||
| }; | }; | ||||
| onMounted(() => { | |||||
| onMounted( () => { | |||||
| getBlog(); | getBlog(); | ||||
| handleSearch() | |||||
| }); | }); | ||||
| const submitForm = () => { | const submitForm = () => { | ||||
| @@ -315,45 +410,144 @@ export default { | |||||
| loading.value = true; | loading.value = true; | ||||
| const formData = new FormData(); | |||||
| formData.append("title", title.value); | |||||
| formData.append("slug", slug.value); | |||||
| formData.append("summary", summary.value); | |||||
| formData.append("content", editorContent.value); | |||||
| if (image.value) { | |||||
| formData.append("image", image.value); | |||||
| } | |||||
| const params = { | |||||
| title: title.value, | |||||
| slug: slug.value, | |||||
| content: editorContent.value, | |||||
| author: author.value, | |||||
| summary: summary.value, | |||||
| locale: locale.value, | |||||
| }; | |||||
| formData.append("author", author.value); | |||||
| formData.append("_method", "put"); | |||||
| formData.append("blog_category_id", blogCat.value); | |||||
| const existingTranslation = blog.value?.translations?.find( | |||||
| t => t.locale === locale.value | |||||
| ); | |||||
| ApiServiece.post(`admin/blogs/${route.params.id}`, formData, { | |||||
| const url = existingTranslation ? `admin/blogs/${route.params.id}/translations/${existingTranslation?.id}` : `admin/blogs/${route.params.id}/translations` | |||||
| ApiServiece[existingTranslation ? 'put' : 'post'](url, params, { | |||||
| headers: { | headers: { | ||||
| "content-type": "multipart", | |||||
| Authorization: `Bearer ${localStorage.getItem("token")}`, | Authorization: `Bearer ${localStorage.getItem("token")}`, | ||||
| }, | }, | ||||
| }) | }) | ||||
| .then((resp) => { | .then((resp) => { | ||||
| console.log(resp); | |||||
| toast.success("!بلاگ با موفقیت ویرایش شد", { | |||||
| const updatedCategory = blog.value; | |||||
| updatedCategory.translations = resp?.data?.data?.translations | |||||
| blog.value = updatedCategory; | |||||
| toast.success(resp?.data?.message, { | |||||
| position: "top-right", | position: "top-right", | ||||
| autoClose: 1000, | autoClose: 1000, | ||||
| }); | }); | ||||
| }) | }) | ||||
| .catch((error) => { | .catch((error) => { | ||||
| console.error(error); | |||||
| toast.error("!مشکلی در ویرایش باگ پیش آمد", { | |||||
| toast.error(error?.response?.data?.message, { | |||||
| position: "top-right", | position: "top-right", | ||||
| autoClose: 1000, | autoClose: 1000, | ||||
| }); | |||||
| }) | |||||
| }) | }) | ||||
| .finally(() => { | .finally(() => { | ||||
| loading.value = false; | loading.value = false; | ||||
| }); | }); | ||||
| }; | }; | ||||
| const handlerChangeLocale = (e) => { | |||||
| const findLocale = blog.value?.translations?.find(item => item?.locale === e.target.value); | |||||
| if (findLocale) { | |||||
| title.value = findLocale?.title; | |||||
| slug.value = findLocale?.slug; | |||||
| summary.value = findLocale?.summary; | |||||
| author.value = findLocale?.author; | |||||
| locale.value = findLocale?.locale; | |||||
| blogTranslationId.value = findLocale?.id | |||||
| quillInstance.value.root.innerHTML = findLocale?.content | |||||
| } else { | |||||
| title.value = undefined; | |||||
| slug.value = undefined; | |||||
| summary.value = undefined; | |||||
| author.value = undefined; | |||||
| quillInstance.value.root.innerHTML = undefined | |||||
| } | |||||
| } | |||||
| const handlerSubmitCategory = async () => { | |||||
| try { | |||||
| loadingFirstTab.value = true; | |||||
| const formData = new FormData(); | |||||
| if (image.value) { | |||||
| formData.append("image", image.value); | |||||
| } | |||||
| formData.append("blog_categories", blogCat.value); | |||||
| formData.append("_method", 'put'); | |||||
| const { data: { success, message } } = await ApiServiece.post( | |||||
| `admin/blogs/${blog.value?.id}`, formData, { | |||||
| headers: { | |||||
| "content-type": "multipart", | |||||
| Authorization: `Bearer ${localStorage.getItem("token")}`, | |||||
| }, | |||||
| }); | |||||
| if(success) { | |||||
| /* categoryId.value = data?.id | |||||
| emit("cat-updated")*/ | |||||
| toast.success(message) | |||||
| } | |||||
| } catch (e) { | |||||
| toast.error(e?.response?.data?.message) | |||||
| } finally { | |||||
| loadingFirstTab.value = false; | |||||
| } | |||||
| }; | |||||
| const handlerRemoveTranslation = async () => { | |||||
| const findLocale = blog.value?.translations?.find(item => item?.locale === locale.value); | |||||
| if (findLocale) { | |||||
| try { | |||||
| loadingDelete.value = true; | |||||
| const { data: { success, message, data } } = await ApiServiece.delete( | |||||
| `admin/blogs/${blog.value?.id}/translations/${findLocale?.id}` | |||||
| ) | |||||
| if (success) { | |||||
| const updatedCategory = blog.value | |||||
| updatedCategory.translations = data?.translations | |||||
| blog.value = updatedCategory | |||||
| toast.success(message) | |||||
| } | |||||
| } catch (e) { | |||||
| console.log(e) | |||||
| }finally { | |||||
| loadingDelete.value = false | |||||
| } | |||||
| } | |||||
| } | |||||
| return { | return { | ||||
| title, | title, | ||||
| slug, | slug, | ||||
| @@ -371,7 +565,16 @@ export default { | |||||
| author, | author, | ||||
| blogCat, | blogCat, | ||||
| loading, | loading, | ||||
| loadingFirstTab, | |||||
| handleSearch, | handleSearch, | ||||
| categorySelectorLoader, | |||||
| locale, | |||||
| handlerChangeLocale, | |||||
| blogTranslationId, | |||||
| handlerSubmitCategory, | |||||
| findLocaleTranslation, | |||||
| handlerRemoveTranslation, | |||||
| loadingDelete, | |||||
| }; | }; | ||||
| }, | }, | ||||
| }; | }; | ||||
| @@ -240,25 +240,25 @@ export default { | |||||
| <tr v-for="brand in brands" :key="brand.id"> | <tr v-for="brand in brands" :key="brand.id"> | ||||
| <td v-if="brand.image"> | <td v-if="brand.image"> | ||||
| <img | <img | ||||
| :src="brand.image" | |||||
| :src="brand?.image" | |||||
| alt="Brand Image" | alt="Brand Image" | ||||
| class="Brand-Image" | class="Brand-Image" | ||||
| /> | /> | ||||
| </td> | </td> | ||||
| <td v-if="!brand.image">ندارد</td> | <td v-if="!brand.image">ندارد</td> | ||||
| <td>{{ brand.title }}</td> | |||||
| <td>{{ brand?.translation?.title }}</td> | |||||
| <td> | <td> | ||||
| <div | <div | ||||
| type="button" | type="button" | ||||
| data-bs-target="#showDescription" | data-bs-target="#showDescription" | ||||
| data-bs-toggle="modal" | data-bs-toggle="modal" | ||||
| @click="descriptionModal(brand?.description)" | |||||
| @click="descriptionModal(brand?.translation?.description)" | |||||
| class="subject-box" | class="subject-box" | ||||
| > | > | ||||
| <i class="fas fa-comments subject-icon"></i> | <i class="fas fa-comments subject-icon"></i> | ||||
| <span class="subject-text"> | <span class="subject-text"> | ||||
| {{ brand?.description.slice(0, 20) | |||||
| }}{{ brand?.description.length > 20 ? "..." : "" }} | |||||
| {{ brand?.translation?.description?.slice(0, 20)}} | |||||
| {{ brand?.translation?.description?.length > 20 ? "..." : "" }} | |||||
| </span> | </span> | ||||
| </div> | </div> | ||||
| </td> | </td> | ||||
| @@ -347,13 +347,13 @@ export default { | |||||
| <!-- Trailing dots and last page --> | <!-- Trailing dots and last page --> | ||||
| <li | <li | ||||
| v-if="visiblePages[visiblePages.length - 1] < totalPages - 1" | |||||
| v-if="visiblePages[visiblePages?.length - 1] < totalPages - 1" | |||||
| class="page-item disabled" | class="page-item disabled" | ||||
| > | > | ||||
| <span class="page-link">...</span> | <span class="page-link">...</span> | ||||
| </li> | </li> | ||||
| <li | <li | ||||
| v-if="visiblePages[visiblePages.length - 1] < totalPages" | |||||
| v-if="visiblePages[visiblePages?.length - 1] < totalPages" | |||||
| class="page-item" | class="page-item" | ||||
| @click="page = totalPages" | @click="page = totalPages" | ||||
| > | > | ||||
| @@ -3,13 +3,15 @@ import Layout from "@/layout/custom.vue"; | |||||
| import ApiServiece from "@/services/ApiService"; | import ApiServiece from "@/services/ApiService"; | ||||
| import moment from "jalali-moment"; | import moment from "jalali-moment"; | ||||
| import { onMounted, ref, watch, computed } from "vue"; | |||||
| import {onMounted, ref, watch, computed, nextTick} from "vue"; | |||||
| import { toast } from "vue3-toastify"; | import { toast } from "vue3-toastify"; | ||||
| import "vue3-toastify/dist/index.css"; | import "vue3-toastify/dist/index.css"; | ||||
| import Swal from "sweetalert2"; | import Swal from "sweetalert2"; | ||||
| import addCat from "@/components/modals/categories/addCat.vue"; | import addCat from "@/components/modals/categories/addCat.vue"; | ||||
| import showDescription from "@/components/modals/commonModals/showDescription.vue"; | import showDescription from "@/components/modals/commonModals/showDescription.vue"; | ||||
| import editCat from "@/components/modals/categories/editCat.vue"; | import editCat from "@/components/modals/categories/editCat.vue"; | ||||
| import { Modal } from "bootstrap" | |||||
| export default { | export default { | ||||
| name: "BORDER", | name: "BORDER", | ||||
| components: { | components: { | ||||
| @@ -34,6 +36,8 @@ export default { | |||||
| const catId = ref(); | const catId = ref(); | ||||
| const catParent = ref(); | const catParent = ref(); | ||||
| const catImage = ref(); | const catImage = ref(); | ||||
| const categoryRow = ref(); | |||||
| const convertToJalali = (date) => { | const convertToJalali = (date) => { | ||||
| return moment(date, "YYYY-MM-DD HH:mm:ss") | return moment(date, "YYYY-MM-DD HH:mm:ss") | ||||
| .locale("fa") | .locale("fa") | ||||
| @@ -119,9 +123,9 @@ export default { | |||||
| const handleCatUpdated = () => { | const handleCatUpdated = () => { | ||||
| getCats(); | getCats(); | ||||
| }; | }; | ||||
| const deleteCat = (id, title) => { | |||||
| const deleteCat = (id) => { | |||||
| Swal.fire({ | Swal.fire({ | ||||
| text: `می خواهید دسته ${title} را حذف کنید؟`, | |||||
| text: `می خواهید دسته را حذف کنید؟`, | |||||
| icon: "warning", | icon: "warning", | ||||
| showCancelButton: true, | showCancelButton: true, | ||||
| confirmButtonColor: "#3085d6", | confirmButtonColor: "#3085d6", | ||||
| @@ -149,13 +153,17 @@ export default { | |||||
| }); | }); | ||||
| }; | }; | ||||
| const editModalData = (id, title, desc, parent, img, icon) => { | |||||
| catId.value = id; | |||||
| catTitle.value = title; | |||||
| catDescription.value = desc; | |||||
| catParent.value = parent; | |||||
| catImage.value = img; | |||||
| catIcon.value = icon; | |||||
| const editModalData = async (category) => { | |||||
| // nextTick(() => { | |||||
| // if (category) | |||||
| // categoryRow.value = category | |||||
| // }) | |||||
| categoryRow.value = category; | |||||
| await nextTick(); | |||||
| // Trigger modal show after data is ready | |||||
| const modal = new Modal(document.getElementById('editCat')); | |||||
| modal.show(); | |||||
| }; | }; | ||||
| const descriptionModal = (desc) => { | const descriptionModal = (desc) => { | ||||
| @@ -233,6 +241,7 @@ export default { | |||||
| visiblePages, | visiblePages, | ||||
| catIcon, | catIcon, | ||||
| restoreCat, | restoreCat, | ||||
| categoryRow, | |||||
| }; | }; | ||||
| }, | }, | ||||
| }; | }; | ||||
| @@ -286,7 +295,7 @@ export default { | |||||
| /> | /> | ||||
| </td> | </td> | ||||
| <td v-if="!cat.image">ندارد</td> | <td v-if="!cat.image">ندارد</td> | ||||
| <td>{{ cat.title }}</td> | |||||
| <td>{{ cat?.translation?.title ?? 'بدون نام' }}</td> | |||||
| <td> | <td> | ||||
| <div | <div | ||||
| type="button" | type="button" | ||||
| @@ -307,32 +316,21 @@ export default { | |||||
| <td v-if="!cat?.parent?.title">ندارد</td> | <td v-if="!cat?.parent?.title">ندارد</td> | ||||
| <td> | <td> | ||||
| <button | <button | ||||
| @click=" | |||||
| editModalData( | |||||
| cat?.id, | |||||
| cat?.title, | |||||
| cat.description, | |||||
| cat?.parent?.id, | |||||
| cat?.image, | |||||
| cat?.icon | |||||
| ) | |||||
| " | |||||
| data-bs-toggle="modal" | |||||
| data-bs-target="#editCat" | |||||
| @click="editModalData(cat)" | |||||
| class="btn btn-sm btn-outline-warning me-1" | class="btn btn-sm btn-outline-warning me-1" | ||||
| > | > | ||||
| ویرایش | ویرایش | ||||
| </button> | </button> | ||||
| <button | <button | ||||
| v-if="!cat.deleted_at" | v-if="!cat.deleted_at" | ||||
| @click="deleteCat(cat.id, cat.title)" | |||||
| @click="deleteCat(cat?.id)" | |||||
| class="btn btn-sm btn-outline-danger" | class="btn btn-sm btn-outline-danger" | ||||
| > | > | ||||
| حذف | حذف | ||||
| </button> | </button> | ||||
| <button | <button | ||||
| v-else | v-else | ||||
| @click="restoreCat(cat?.id, cat?.title)" | |||||
| @click="restoreCat(cat.id)" | |||||
| class="btn btn-sm btn-outline-success" | class="btn btn-sm btn-outline-success" | ||||
| > | > | ||||
| بازیابی | بازیابی | ||||
| @@ -351,13 +349,8 @@ export default { | |||||
| </div> | </div> | ||||
| <addCat :parents="cats" @cat-updated="handleCatUpdated()" /> | <addCat :parents="cats" @cat-updated="handleCatUpdated()" /> | ||||
| <editCat | <editCat | ||||
| :id="catId" | |||||
| :title="catTitle" | |||||
| :description="catDescription" | |||||
| :parent="catParent" | |||||
| :image="catImage" | |||||
| :allParents="cats" | |||||
| :icon="catIcon" | |||||
| :parents="cats" | |||||
| :categoryRow="categoryRow" | |||||
| @cat-updated="handleCatUpdated()" | @cat-updated="handleCatUpdated()" | ||||
| /> | /> | ||||
| <showDescription :desc="catDescription" /> | <showDescription :desc="catDescription" /> | ||||
| @@ -31,6 +31,8 @@ export default { | |||||
| const userId = ref(); | const userId = ref(); | ||||
| const userRole = ref(); | const userRole = ref(); | ||||
| const selectedRole = ref(""); | const selectedRole = ref(""); | ||||
| const userProfile = JSON.parse(localStorage.getItem('user_profile')); | |||||
| const convertToJalali = (date) => { | const convertToJalali = (date) => { | ||||
| return moment(date, "YYYY-MM-DD HH:mm:ss") | return moment(date, "YYYY-MM-DD HH:mm:ss") | ||||
| .locale("fa") | .locale("fa") | ||||
| @@ -234,6 +236,7 @@ export default { | |||||
| selectedRole, | selectedRole, | ||||
| selectedStatus, | selectedStatus, | ||||
| filterLoading, | filterLoading, | ||||
| userProfile, | |||||
| }; | }; | ||||
| }, | }, | ||||
| }; | }; | ||||
| @@ -293,6 +296,7 @@ export default { | |||||
| <!-- Add User Button --> | <!-- Add User Button --> | ||||
| </div> | </div> | ||||
| <button | <button | ||||
| v-if="userProfile?.role === 'admin'" | |||||
| data-bs-toggle="modal" | data-bs-toggle="modal" | ||||
| data-bs-target="#addUser" | data-bs-target="#addUser" | ||||
| class="btn btn-add-user btn btn-light text-primary btn-sm px-3" | class="btn btn-add-user btn btn-light text-primary btn-sm px-3" | ||||