Переглянути джерело

compketed category blog and products and blogs

master
alireza 5 місяці тому
джерело
коміт
f9c7448d3d
25 змінених файлів з 9388 додано та 5213 видалено
  1. +1
    -1
      .env
  2. +8
    -0
      .idea/.gitignore
  3. +6
    -0
      .idea/inspectionProfiles/Project_Default.xml
  4. +8
    -0
      .idea/modules.xml
  5. +12
    -0
      .idea/truck-admin.iml
  6. +6
    -0
      .idea/vcs.xml
  7. +7155
    -3462
      package-lock.json
  8. +3
    -1
      package.json
  9. +29
    -1
      src/assets/scss/style.scss
  10. +178
    -84
      src/components/modals/blogCat/addBlogCat.vue
  11. +65
    -60
      src/components/modals/blogCat/editBlogCat.vue
  12. +203
    -214
      src/components/modals/categories/addCat.vue
  13. +375
    -428
      src/components/modals/categories/editCat.vue
  14. +6
    -1
      src/state/modules/user.js
  15. +20
    -0
      src/utils/useModal.js
  16. +0
    -1
      src/views/live-preview/pages/auth2/login.vue
  17. +5
    -1
      src/views/live-preview/pages/banners/editBanner.vue
  18. +2
    -7
      src/views/live-preview/pages/blogCats/blogCat.vue
  19. +325
    -205
      src/views/live-preview/pages/blogs/addBlog.vue
  20. +5
    -6
      src/views/live-preview/pages/blogs/blogs.vue
  21. +380
    -177
      src/views/live-preview/pages/blogs/editBlog.vue
  22. +7
    -7
      src/views/live-preview/pages/brands/brands.vue
  23. +25
    -32
      src/views/live-preview/pages/catrgories/cats.vue
  24. +560
    -525
      src/views/live-preview/pages/products/addProduct.vue
  25. +4
    -0
      src/views/live-preview/pages/users/users.vue

+ 1
- 1
.env Переглянути файл

@@ -1 +1 @@
VUE_APP_ROOT_URL="https://api.novinplast.org/api/v1/"
VUE_APP_ROOT_URL = http://192.168.100.126:8000/api/v1/

+ 8
- 0
.idea/.gitignore Переглянути файл

@@ -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

+ 6
- 0
.idea/inspectionProfiles/Project_Default.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>

+ 8
- 0
.idea/modules.xml Переглянути файл

@@ -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>

+ 12
- 0
.idea/truck-admin.iml Переглянути файл

@@ -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>

+ 6
- 0
.idea/vcs.xml Переглянути файл

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
</component>
</project>

+ 7155
- 3462
package-lock.json
Різницю між файлами не показано, бо вона завелика
Переглянути файл


+ 3
- 1
package.json Переглянути файл

@@ -29,7 +29,7 @@
"@zhuowenli/vue-feather-icons": "^5.0.2",
"add": "^2.0.6",
"aos": "^2.3.4",
"apexcharts": "^3.44.2",
"apexcharts": "^5.3.2",
"axios": "^1.7.9",
"bootstrap": "^5.3.2",
"bootstrap-datepicker": "^1.10.0",
@@ -64,12 +64,14 @@
"vue-flatpickr-component": "^11.0.3",
"vue-masonry": "^0.16.0",
"vue-router": "^4.2.5",
"vue-simple-stepper": "^1.1.1",
"vue3-apexcharts": "^1.4.4",
"vue3-datepicker": "^0.4.0",
"vue3-google-map": "^0.18.0",
"vue3-persian-datetime-picker": "^1.2.2",
"vue3-select-component": "^0.11.3",
"vue3-select2-component": "^0.1.7",
"vue3-steppy": "^1.5.8",
"vue3-toastify": "^0.2.5",
"vuex": "^4.1.0",
"yarn": "^1.22.21"


+ 29
- 1
src/assets/scss/style.scss Переглянути файл

@@ -161,4 +161,32 @@ File: style.css
@import 'themes/layouts/customizer';

@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;
}

+ 178
- 84
src/components/modals/blogCat/addBlogCat.vue Переглянути файл

@@ -21,84 +21,136 @@
></button>
</div>
<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>
</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>
</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>
@@ -111,14 +163,22 @@ import { ref } from "vue";
import { toast } from "vue3-toastify";
import "vue3-toastify/dist/index.css";
import ApiServiece from "@/services/ApiService";
import {Steppy} from "vue3-steppy";

export default {
components: {Steppy},
props: {},
setup(props, { emit }) {
const title = ref();
const selectedIcon = ref();
const errors = ref({});
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) => {
errors.value[field] = "";
@@ -127,7 +187,7 @@ export default {
const validateForm = () => {
errors.value = {};
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;
};

@@ -135,8 +195,34 @@ export default {
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 = () => {
console.log(selectedIcon.value);
if (!validateForm()) {
toast.error("لطفا فیلد های لازم را وارد نمایید", {
position: "top-right",
@@ -146,12 +232,14 @@ export default {
}
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",
autoClose: 1000,
});
@@ -161,7 +249,7 @@ export default {
document.getElementById("closeAddBlogCat").click();
emit("cat-updated");
title.value = "";
selectedIcon.value = "";
step.value = 1;
}, 500);
})
.catch((error) => {
@@ -183,9 +271,15 @@ export default {
addCat,
title,
selectedIcon,
locale,
iconData,
setSelectedIcon,
editMode,
titleCat,
step,
handlerAddCategory,
loadingStep,
blogCategory,
};
},
};


+ 65
- 60
src/components/modals/blogCat/editBlogCat.vue Переглянути файл

@@ -19,62 +19,63 @@
></button>
</div>
<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">
<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>
<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 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>
</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>
</div>
</BCol>
@@ -82,24 +83,24 @@

<!-- Submit Buttons -->
<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
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 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>
</div>
@@ -138,6 +139,8 @@ export default {
const localId = toRef(props.id);
const errors = ref({});
const loading = ref(false);
const titleCat = ref(null);
const title = ref(null);

watch(
() => props.title,
@@ -218,6 +221,8 @@ export default {
setSelectedIcon,
localIcon,
localId,
title,
titleCat,
};
},
};


+ 203
- 214
src/components/modals/categories/addCat.vue Переглянути файл

@@ -21,165 +21,158 @@
></button>
</div>
<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>

<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>

<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>
</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>
@@ -188,12 +181,14 @@

<script>
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 "vue3-toastify/dist/index.css";
import ApiServiece from "@/services/ApiService";
import { Steppy } from "vue3-steppy"

export default {
components: { Steppy },
props: {
parents: {
type: Array,
@@ -205,56 +200,21 @@ export default {
const selectedPaernt = ref();
const localParents = toRef(props.parents);
const image = ref(null);
const imagePreview = ref(null);
const description = ref();
const title = ref();
const errors = ref({});
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(
() => 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) => {
selectedIcon.value = icon;
@@ -265,8 +225,6 @@ export default {
if (!description.value)
errors.value.description = "وارد کردن توضیحات ضروری می باشد";
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;
};

@@ -285,39 +243,36 @@ export default {
loading.value = true;

const formData = new FormData();

formData.append("title", title.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: {
"content-type": "multipart",
Authorization: `Bearer ${localStorage.getItem("token")}`,
},
})
.then((resp) => {
console.log(resp);
toast.success("!دسته با موفقیت اضافه شد", {
console.log(resp,'resp resp')
toast.success(resp?.data?.message, {
position: "top-right",
autoClose: 1000,
});
})
.then(() => {
setTimeout(() => {
document.getElementById("close").click();
//document.getElementById("close").click();
emit("cat-updated");
title.value = "";
imagePreview.value = null;
image.value = null;
description.value = "";
selectedPaernt.value = ""
selectedIcon.value = "";
imageFileRef.value.value = null
}, 500);
})
.catch((error) => {
console.error(error);
toast.error(`${error.response.data.message}`, {
position: "top-right",
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 {
errors,
loading,
clearError,
addCat,
handleImageChange,
image,
imagePreview,
removeImage,
title,
description,
localParents,
@@ -344,13 +329,16 @@ export default {
setSelectedIcon,
iconData,
selectedIcon,
imageFileRef,
locale,
step,
loadingStep,
handlerSubmitCategory
};
},
};
</script>

<style scoped>
<style>
.modal-dialog {
max-width: 50%;
}
@@ -455,4 +443,5 @@ export default {
align-items: center;
margin: 8px;
}

</style>

+ 375
- 428
src/components/modals/categories/editCat.vue Переглянути файл

@@ -1,184 +1,166 @@
<template>
<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-content">
<div class="modal-header">
<h5 class="modal-title" id="exampleModalLabel">ویرایش دسته</h5>

<h5 class="modal-title" id="exampleModalLabel">
ویرایش
</h5>
<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>
</div>
<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>

<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 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>

<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>
</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>
@@ -186,133 +168,91 @@
</template>

<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 "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 {
components: {BButton},
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,
Required: true,
},
icon: {
type: String,
categoryRow: {
type: Object,
Required: true,
},
},

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 localId = toRef(props.id);
const description = ref();
const title = ref();
const errors = ref({});
const imageFileRef = ref(null);

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(
() => 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 = () => {
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;
};

@@ -320,8 +260,7 @@ export default {
errors.value[field] = "";
};

const editCat = () => {
console.log(localId.value);
const editCat = async () => {
if (!validateForm()) {
toast.error("لطفا فیلد های لازم را وارد نمایید", {
position: "top-right",
@@ -329,91 +268,156 @@ export default {
});
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(() => {
document.getElementById("closeEdit").click();
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 {
errors,
loading,
clearError,
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>

<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 {
max-width: 50%;
}
@@ -422,12 +426,9 @@ export default {
padding: 20px;
}

.modal-header {
border-bottom: 1px solid #dee2e6;
}

.modal-body {
padding: 20px;
padding: 1rem 1.5rem;
}

.form-group {
@@ -437,107 +438,28 @@ export default {
.input-group {
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 {
max-width: 50%;
}

.modal-content {
padding: 1.5rem; /* Increased padding for better spacing */
padding: 1.5rem;
}

.modal-header {
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 {
margin-bottom: 1.5rem; /* Increased margin between form groups */
margin-bottom: 1.5rem;
}

.input-group {
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 {
min-width: 100px;
max-height: 100px;
@@ -573,6 +495,16 @@ export default {
.delete-btn:focus {
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 {
display: flex;
justify-content: center;
@@ -591,15 +523,30 @@ export default {
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>

+ 6
- 1
src/state/modules/user.js Переглянути файл

@@ -29,6 +29,7 @@ export const mutations = {
state.token = null;
state.isAuthenticated = false;
localStorage.removeItem("token");
localStorage.removeItem("user_profile");
},

SET_LOADING(state, status) {
@@ -39,7 +40,7 @@ export const mutations = {
export const actions = {
async loginUser({ commit }, credentials) {
try {
const { data } = await axios.post(`${url}auth/login`, credentials, {
const { data } = await axios.post(`${url}auth/admin/login`, credentials, {
headers: {
"Content-Type": "application/json",
},
@@ -57,6 +58,8 @@ export const actions = {
commit("SET_TOKEN", data.data.token);

localStorage.setItem("token", data.data.token);

localStorage.setItem("user_profile", JSON.stringify(data.data.user));
} else {
throw new Error("شماره موبایل یا رمز عبور اشتباه است");
}
@@ -91,6 +94,8 @@ export const actions = {
if (user?.role === "admin" || user?.role === "operator") {
localStorage.setItem("token", token);

localStorage.setItem("user_profile", JSON.stringify(user));

commit("SET_TOKEN", token);

commit("SET_USER", user);


+ 20
- 0
src/utils/useModal.js Переглянути файл

@@ -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 };
}

+ 0
- 1
src/views/live-preview/pages/auth2/login.vue Переглянути файл

@@ -108,7 +108,6 @@ export default {
password: password.value,
});
loading.value = false;
console.log("Login successful");
router.push({ name: "products" });
} catch (error) {
console.error("Login failed:", error.message);


+ 5
- 1
src/views/live-preview/pages/banners/editBanner.vue Переглянути файл

@@ -702,8 +702,12 @@ export default {
}

formData.append("location", selectedLoc.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) {
formData.append("image", image.value);
}


+ 2
- 7
src/views/live-preview/pages/blogCats/blogCat.vue Переглянути файл

@@ -218,7 +218,6 @@ export default {
<table class="table table-hover table-bordered m-0" dir="rtl">
<thead class="table-light">
<tr>
<th>آیکن</th>
<th>عنوان</th>
<th>تاریخ ایجاد</th>
<th>عملیات</th>
@@ -226,17 +225,13 @@ export default {
</thead>
<tbody>
<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>
<button
@click="editModalData(cat?.id, cat?.title, cat?.icon)"
@click="editModalData(cat?.id, cat?.translation?.title)"
data-bs-toggle="modal"
data-bs-target="#editBlogCat"
class="btn btn-sm btn-outline-warning me-1"


+ 325
- 205
src/views/live-preview/pages/blogs/addBlog.vue Переглянути файл

@@ -7,155 +7,227 @@
<h5>ایجاد بلاگ</h5>
</BCardHeader>
<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
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>
</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>
<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>
</BCol>
</BRow>
@@ -167,19 +239,24 @@ import VueSelect from "vue3-select-component";
import { toast } from "vue3-toastify";
import "vue3-toastify/dist/index.css";
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 Quill from "quill";
import "quill/dist/quill.snow.css";
import {Steppy} from "vue3-steppy";
import {BRow} from "bootstrap-vue-next";

export default {
name: "SAMPLE-PAGE",
components: {
BRow,
Steppy,
Layout,
VueSelect,
},
setup() {
const loading = ref(false);
const loadingStep = ref(false);
const image = ref();
const imagePreview = ref();
const errors = ref({});
@@ -192,9 +269,9 @@ export default {
const categories = ref([]);
const editorContent = ref("");
const categorySelectorLoader = ref(false)
const step = ref(1)
const blogCategoryId = ref(null)
const locale = ref('fa')

const handleSearch = async (searchTerm) => {
if (searchTerm.length < 3) return;
@@ -214,8 +291,8 @@ export default {
const formattedCategories = computed(() =>
Array.isArray(categories.value)
? 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 = "وارد کردن کلمه کلیدی بلاگ ضروری می باشد";
if (!summary.value)
errors.value.summary = "وارد کردن خلاصه بلاگ ضروری می باشد";
if (!blogCat.value)
errors.value.blogCat = "انتخاب دسته برای بلاگ ضروری می باشد";
if (!author.value)
errors.value.author = "وارد کردن نویسنده بلاگ ضروری می باشد";
if (!editorContent.value)
errors.value.editorContent = "وارد کردن محتوای بلاگ ضروری می باشد";
if (!image.value) errors.value.image = "وارد کردن عکس بلاگ ضروری می باشد";
return Object.keys(errors.value).length === 0;
};

@@ -267,39 +341,6 @@ export default {
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 = () => {
if (!validateForm()) {
toast.error("لطفا فیلد های لازم را وارد نمایید", {
@@ -311,33 +352,34 @@ export default {

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: {
"content-type": "multipart",
Authorization: `Bearer ${localStorage.getItem("token")}`,
},
})
.then(() => {
toast.success("!بلاگ با موفقیت اضافه شد", {
.then(({ data }) => {
toast.success(data?.message, {
position: "top-right",
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("!مشکلی در اضافه کردن بلاگ پیش آمد", {
position: "top-right",
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 {
title,
slug,
@@ -365,9 +480,14 @@ export default {
author,
blogCat,
loading,
loadingStep,
step,
handleSearch,
categorySelectorLoader,
formattedCategories
formattedCategories,
handlerAddCategory,
blogCategoryId,
locale
};
},
};


+ 5
- 6
src/views/live-preview/pages/blogs/blogs.vue Переглянути файл

@@ -93,7 +93,7 @@ export default {
});
const deleteBlog = (id, title) => {
Swal.fire({
text: `می خواهید بلاگ ${title} را حذف کنید؟`,
text: `می خواهید بلاگ ${title ?? ''} را حذف کنید؟`,
icon: "warning",
showCancelButton: true,
confirmButtonColor: "#3085d6",
@@ -110,8 +110,7 @@ export default {
});
blogs.value = blogs.value.filter((blog) => blog.id !== id);
})
.catch((err) => {
console.log(err);
.catch(() => {
toast.error("!مشکلی در حذف کردن بلاگ پیش آمد", {
position: "top-right",
autoClose: 3000,
@@ -220,8 +219,8 @@ export default {
/>
</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>
<router-link
@@ -231,7 +230,7 @@ export default {
ویرایش
</router-link>
<button
@click="deleteBlog(blog?.id, blog?.title)"
@click="deleteBlog(blog?.id, blog?.translation?.title)"
class="btn btn-sm btn-outline-danger"
>
حذف


+ 380
- 177
src/views/live-preview/pages/blogs/editBlog.vue Переглянути файл

@@ -7,151 +7,207 @@
<h5>ویرایش بلاگ</h5>
</BCardHeader>
<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>
</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>
<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>
</BCol>
</BRow>
@@ -168,9 +224,11 @@ import Layout from "@/layout/custom.vue";
import Quill from "quill";
import "quill/dist/quill.snow.css";
import { useRoute } from "vue-router";
import {BTabs} from "bootstrap-vue-next";
export default {
name: "SAMPLE-PAGE",
components: {
BTabs,
Layout,
VueSelect,
},
@@ -189,26 +247,51 @@ export default {
const author = ref("");
const editor = ref(null);
const categorySelectorLoader = ref(false);
const categories = ref([{ id: null, title: "" }]);
const categories = ref([{ value: null, label: null }]);
const editorContent = ref("");
const locale = ref("fa");
const blogTranslationId = ref();
const loadingFirstTab = ref(false);
const loadingDelete = ref(false);

const formattedCategories = computed(() =>
Array.isArray(categories.value)
? 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) => {
if (searchTerm.length < 3) return;
if (searchTerm?.length < 3) return;

categorySelectorLoader.value = true;

try {
const response = await ApiServiece.get(
`admin/blog-categories?title=${searchTerm}`
`admin/blog-categories?title=${searchTerm ?? ''}`
);
categories.value = response.data.data;
categorySelectorLoader.value = false;
@@ -249,7 +332,7 @@ export default {
errors.value.editorContent = "وارد کردن محتوای بلاگ ضروری می باشد";
if (!imagePreview.value)
errors.value.image = "وارد کردن عکس بلاگ ضروری می باشد";
return Object.keys(errors.value).length === 0;
return Object.keys(errors.value)?.length === 0;
};

const clearError = (field) => {
@@ -260,16 +343,25 @@ export default {
ApiServiece.get(`admin/blogs/${route.params.id}`)
.then((resp) => {
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) {
quillInstance.value = new Quill(editor.value, {
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
quillInstance.value.on("text-change", () => {
@@ -300,8 +393,10 @@ export default {
});
};

onMounted(() => {
onMounted( () => {
getBlog();

handleSearch()
});

const submitForm = () => {
@@ -315,45 +410,144 @@ export default {

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: {
"content-type": "multipart",
Authorization: `Bearer ${localStorage.getItem("token")}`,
},
})
.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",
autoClose: 1000,
});
})

.catch((error) => {
console.error(error);
toast.error("!مشکلی در ویرایش باگ پیش آمد", {
toast.error(error?.response?.data?.message, {
position: "top-right",
autoClose: 1000,
});
})
})
.finally(() => {
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 {
title,
slug,
@@ -371,7 +565,16 @@ export default {
author,
blogCat,
loading,
loadingFirstTab,
handleSearch,
categorySelectorLoader,
locale,
handlerChangeLocale,
blogTranslationId,
handlerSubmitCategory,
findLocaleTranslation,
handlerRemoveTranslation,
loadingDelete,
};
},
};


+ 7
- 7
src/views/live-preview/pages/brands/brands.vue Переглянути файл

@@ -240,25 +240,25 @@ export default {
<tr v-for="brand in brands" :key="brand.id">
<td v-if="brand.image">
<img
:src="brand.image"
:src="brand?.image"
alt="Brand Image"
class="Brand-Image"
/>
</td>
<td v-if="!brand.image">ندارد</td>
<td>{{ brand.title }}</td>
<td>{{ brand?.translation?.title }}</td>
<td>
<div
type="button"
data-bs-target="#showDescription"
data-bs-toggle="modal"
@click="descriptionModal(brand?.description)"
@click="descriptionModal(brand?.translation?.description)"
class="subject-box"
>
<i class="fas fa-comments subject-icon"></i>
<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>
</div>
</td>
@@ -347,13 +347,13 @@ export default {

<!-- Trailing dots and last page -->
<li
v-if="visiblePages[visiblePages.length - 1] < totalPages - 1"
v-if="visiblePages[visiblePages?.length - 1] < totalPages - 1"
class="page-item disabled"
>
<span class="page-link">...</span>
</li>
<li
v-if="visiblePages[visiblePages.length - 1] < totalPages"
v-if="visiblePages[visiblePages?.length - 1] < totalPages"
class="page-item"
@click="page = totalPages"
>


+ 25
- 32
src/views/live-preview/pages/catrgories/cats.vue Переглянути файл

@@ -3,13 +3,15 @@ import Layout from "@/layout/custom.vue";

import ApiServiece from "@/services/ApiService";
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 "vue3-toastify/dist/index.css";
import Swal from "sweetalert2";
import addCat from "@/components/modals/categories/addCat.vue";
import showDescription from "@/components/modals/commonModals/showDescription.vue";
import editCat from "@/components/modals/categories/editCat.vue";
import { Modal } from "bootstrap"

export default {
name: "BORDER",
components: {
@@ -34,6 +36,8 @@ export default {
const catId = ref();
const catParent = ref();
const catImage = ref();
const categoryRow = ref();

const convertToJalali = (date) => {
return moment(date, "YYYY-MM-DD HH:mm:ss")
.locale("fa")
@@ -119,9 +123,9 @@ export default {
const handleCatUpdated = () => {
getCats();
};
const deleteCat = (id, title) => {
const deleteCat = (id) => {
Swal.fire({
text: `می خواهید دسته ${title} را حذف کنید؟`,
text: `می خواهید دسته را حذف کنید؟`,
icon: "warning",
showCancelButton: true,
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) => {
@@ -233,6 +241,7 @@ export default {
visiblePages,
catIcon,
restoreCat,
categoryRow,
};
},
};
@@ -286,7 +295,7 @@ export default {
/>
</td>
<td v-if="!cat.image">ندارد</td>
<td>{{ cat.title }}</td>
<td>{{ cat?.translation?.title ?? 'بدون نام' }}</td>
<td>
<div
type="button"
@@ -307,32 +316,21 @@ export default {
<td v-if="!cat?.parent?.title">ندارد</td>
<td>
<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"
>
ویرایش
</button>
<button
v-if="!cat.deleted_at"
@click="deleteCat(cat.id, cat.title)"
@click="deleteCat(cat?.id)"
class="btn btn-sm btn-outline-danger"
>
حذف
</button>
<button
v-else
@click="restoreCat(cat?.id, cat?.title)"
@click="restoreCat(cat.id)"
class="btn btn-sm btn-outline-success"
>
بازیابی
@@ -351,13 +349,8 @@ export default {
</div>
<addCat :parents="cats" @cat-updated="handleCatUpdated()" />
<editCat
:id="catId"
:title="catTitle"
:description="catDescription"
:parent="catParent"
:image="catImage"
:allParents="cats"
:icon="catIcon"
:parents="cats"
:categoryRow="categoryRow"
@cat-updated="handleCatUpdated()"
/>
<showDescription :desc="catDescription" />


+ 560
- 525
src/views/live-preview/pages/products/addProduct.vue
Різницю між файлами не показано, бо вона завелика
Переглянути файл


+ 4
- 0
src/views/live-preview/pages/users/users.vue Переглянути файл

@@ -31,6 +31,8 @@ export default {
const userId = ref();
const userRole = ref();
const selectedRole = ref("");
const userProfile = JSON.parse(localStorage.getItem('user_profile'));

const convertToJalali = (date) => {
return moment(date, "YYYY-MM-DD HH:mm:ss")
.locale("fa")
@@ -234,6 +236,7 @@ export default {
selectedRole,
selectedStatus,
filterLoading,
userProfile,
};
},
};
@@ -293,6 +296,7 @@ export default {
<!-- Add User Button -->
</div>
<button
v-if="userProfile?.role === 'admin'"
data-bs-toggle="modal"
data-bs-target="#addUser"
class="btn btn-add-user btn btn-light text-primary btn-sm px-3"


Завантаження…
Відмінити
Зберегти