Compare commits
3 Commits
e9016baf5b
...
110946a924
| Author | SHA1 | Date | |
|---|---|---|---|
| 110946a924 | |||
| 09a77c14c9 | |||
|
|
c7533a8deb |
@@ -18,8 +18,9 @@ type ProductDescription struct {
|
|||||||
AvailableLater string `gorm:"column:available_later;type:varchar(255)" json:"available_later" form:"available_later"`
|
AvailableLater string `gorm:"column:available_later;type:varchar(255)" json:"available_later" form:"available_later"`
|
||||||
DeliveryInStock string `gorm:"column:delivery_in_stock;type:varchar(255)" json:"delivery_in_stock" form:"delivery_in_stock"`
|
DeliveryInStock string `gorm:"column:delivery_in_stock;type:varchar(255)" json:"delivery_in_stock" form:"delivery_in_stock"`
|
||||||
DeliveryOutStock string `gorm:"column:delivery_out_stock;type:varchar(255)" json:"delivery_out_stock" form:"delivery_out_stock"`
|
DeliveryOutStock string `gorm:"column:delivery_out_stock;type:varchar(255)" json:"delivery_out_stock" form:"delivery_out_stock"`
|
||||||
Usage string `gorm:"column:usage;type:text" json:"usage" form:"usage"`
|
Usage string `gorm:"column:_usage_;type:text" json:"usage" form:"usage"`
|
||||||
|
|
||||||
|
ImageLink string `gorm:"column:image_link" json:"image_link"`
|
||||||
ExistsInDatabase bool `gorm:"-" json:"exists_in_database"`
|
ExistsInDatabase bool `gorm:"-" json:"exists_in_database"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
|
"git.ma-al.com/goc_daniel/b2b/app/config"
|
||||||
"git.ma-al.com/goc_daniel/b2b/app/db"
|
"git.ma-al.com/goc_daniel/b2b/app/db"
|
||||||
"git.ma-al.com/goc_daniel/b2b/app/model"
|
"git.ma-al.com/goc_daniel/b2b/app/model"
|
||||||
"git.ma-al.com/goc_daniel/b2b/app/model/dbmodel"
|
"git.ma-al.com/goc_daniel/b2b/app/model/dbmodel"
|
||||||
@@ -36,6 +37,27 @@ func (r *ProductDescriptionRepo) GetProductDescription(productID uint, productid
|
|||||||
IDShop: int32(constdata.SHOP_ID),
|
IDShop: int32(constdata.SHOP_ID),
|
||||||
IDLang: int32(productid_lang),
|
IDLang: int32(productid_lang),
|
||||||
}).
|
}).
|
||||||
|
Select(`
|
||||||
|
`+dbmodel.PsProductLangCols.IDProduct.TabCol()+` AS id_product,
|
||||||
|
`+dbmodel.PsProductLangCols.IDShop.TabCol()+` AS id_shop,
|
||||||
|
`+dbmodel.PsProductLangCols.IDLang.TabCol()+` AS id_lang,
|
||||||
|
`+dbmodel.PsProductLangCols.Description.TabCol()+` AS description,
|
||||||
|
`+dbmodel.PsProductLangCols.DescriptionShort.TabCol()+` AS description_short,
|
||||||
|
`+dbmodel.PsProductLangCols.LinkRewrite.TabCol()+` AS link_rewrite,
|
||||||
|
`+dbmodel.PsProductLangCols.MetaDescription.TabCol()+` AS meta_description,
|
||||||
|
`+dbmodel.PsProductLangCols.MetaKeywords.TabCol()+` AS meta_keywords,
|
||||||
|
`+dbmodel.PsProductLangCols.MetaTitle.TabCol()+` AS meta_title,
|
||||||
|
`+dbmodel.PsProductLangCols.Name.TabCol()+` AS name,
|
||||||
|
`+dbmodel.PsProductLangCols.AvailableNow.TabCol()+` AS available_now,
|
||||||
|
`+dbmodel.PsProductLangCols.AvailableLater.TabCol()+` AS available_later,
|
||||||
|
`+dbmodel.PsProductLangCols.DeliveryInStock.TabCol()+` AS delivery_in_stock,
|
||||||
|
`+dbmodel.PsProductLangCols.DeliveryOutStock.TabCol()+` AS delivery_out_stock,
|
||||||
|
`+dbmodel.PsProductLangCols.Usage.TabCol()+` AS _usage_,
|
||||||
|
CONCAT(?, '/', `+dbmodel.PsImageShopCols.IDImage.TabCol()+`, '-large_default/', `+dbmodel.PsProductLangCols.LinkRewrite.TabCol()+`, '.webp') AS image_link
|
||||||
|
`, config.Get().Image.ImagePrefix).
|
||||||
|
Joins("JOIN " + dbmodel.TableNamePsImageShop +
|
||||||
|
" ON " + dbmodel.PsImageShopCols.IDProduct.TabCol() + "=" + dbmodel.PsProductLangCols.IDProduct.TabCol() +
|
||||||
|
" AND " + dbmodel.PsImageShopCols.Cover.TabCol() + " = 1").
|
||||||
First(&ProductDescription).Error
|
First(&ProductDescription).Error
|
||||||
|
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
|||||||
2
bo/components.d.ts
vendored
2
bo/components.d.ts
vendored
@@ -34,6 +34,7 @@ declare module 'vue' {
|
|||||||
ProductCustomization: typeof import('./src/components/customer/components/ProductCustomization.vue')['default']
|
ProductCustomization: typeof import('./src/components/customer/components/ProductCustomization.vue')['default']
|
||||||
ProductDetailView: typeof import('./src/components/admin/ProductDetailView.vue')['default']
|
ProductDetailView: typeof import('./src/components/admin/ProductDetailView.vue')['default']
|
||||||
'ProductDetailView copy': typeof import('./src/components/admin/ProductDetailView copy.vue')['default']
|
'ProductDetailView copy': typeof import('./src/components/admin/ProductDetailView copy.vue')['default']
|
||||||
|
ProductEditor: typeof import('./src/components/inner/ProductEditor.vue')['default']
|
||||||
ProductVariants: typeof import('./src/components/customer/components/ProductVariants.vue')['default']
|
ProductVariants: typeof import('./src/components/customer/components/ProductVariants.vue')['default']
|
||||||
RouterLink: typeof import('vue-router')['RouterLink']
|
RouterLink: typeof import('vue-router')['RouterLink']
|
||||||
RouterView: typeof import('vue-router')['RouterView']
|
RouterView: typeof import('vue-router')['RouterView']
|
||||||
@@ -62,5 +63,6 @@ declare module 'vue' {
|
|||||||
USidebar: typeof import('./node_modules/@nuxt/ui/dist/runtime/components/Sidebar.vue')['default']
|
USidebar: typeof import('./node_modules/@nuxt/ui/dist/runtime/components/Sidebar.vue')['default']
|
||||||
UTable: typeof import('./node_modules/@nuxt/ui/dist/runtime/components/Table.vue')['default']
|
UTable: typeof import('./node_modules/@nuxt/ui/dist/runtime/components/Table.vue')['default']
|
||||||
UTabs: typeof import('./node_modules/@nuxt/ui/dist/runtime/components/Tabs.vue')['default']
|
UTabs: typeof import('./node_modules/@nuxt/ui/dist/runtime/components/Tabs.vue')['default']
|
||||||
|
UTextarea: typeof import('./node_modules/@nuxt/ui/dist/runtime/components/Textarea.vue')['default']
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -56,6 +56,11 @@ export const uiOptions: NuxtUIOptions = {
|
|||||||
content: 'border! border-(--border-light)! dark:border-(--border-dark)! outline-0! ring-0! bg-(--second-light) dark:bg-(--main-dark)',
|
content: 'border! border-(--border-light)! dark:border-(--border-dark)! outline-0! ring-0! bg-(--second-light) dark:bg-(--main-dark)',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
},
|
||||||
|
textarea: {
|
||||||
|
slots: {
|
||||||
|
base: 'ring-0! outline-0! rounded-md border border-(--border-light)! dark:border-(--border-dark)! text-base!'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -6,9 +6,8 @@
|
|||||||
Back to products</p>
|
Back to products</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div
|
<div class="gap-4 mb-6 border-b border-(--border-light) dark:border-(--border-dark) py-4">
|
||||||
class="gap-4 mb-6 bg-slate-50 dark:bg-(--main-dark) border border-(--border-light) dark:border-(--border-dark) p-4 rounded-md">
|
<div class="flex flex-wrap gap-4 items-center justify-between">
|
||||||
<div class="flex items-center justify-between">
|
|
||||||
<div class="flex flex-col items-center gap-3" v-if="!isTranslations">
|
<div class="flex flex-col items-center gap-3" v-if="!isTranslations">
|
||||||
<USelectMenu v-model="toLangId" :items="langs" value-key="id"
|
<USelectMenu v-model="toLangId" :items="langs" value-key="id"
|
||||||
class="w-48 bg-(--main-light) dark:bg-(--black) rounded-md shadow-sm" :searchInput="false">
|
class="w-48 bg-(--main-light) dark:bg-(--black) rounded-md shadow-sm" :searchInput="false">
|
||||||
@@ -18,7 +17,7 @@
|
|||||||
Selected language
|
Selected language
|
||||||
</span>
|
</span>
|
||||||
<span class="font-medium dark:text-white text-black">
|
<span class="font-medium dark:text-white text-black">
|
||||||
{{langs.find(l => l.id === toLangId)?.name || 'Select language'}}
|
{{ selectedLangName }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -34,7 +33,8 @@
|
|||||||
</USelectMenu>
|
</USelectMenu>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="toLangId !== settingStore.shopDefaultLanguage && !isTranslations" class="flex gap-7">
|
<div v-if="toLangId !== settingStore.shopDefaultLanguage && !isTranslations"
|
||||||
|
class="flex flex-wrap-reverse justify-between gap-2">
|
||||||
<UButton color="info" variant="outline" v-if="!isEditing" @click="isTranslations = true">
|
<UButton color="info" variant="outline" v-if="!isEditing" @click="isTranslations = true">
|
||||||
<p>Change Text</p>
|
<p>Change Text</p>
|
||||||
<UIcon name="material-symbols-light:stylus-note-sharp" class="text-7.5" />
|
<UIcon name="material-symbols-light:stylus-note-sharp" class="text-7.5" />
|
||||||
@@ -45,13 +45,16 @@
|
|||||||
</div>
|
</div>
|
||||||
<div v-if="isTranslations" class="flex gap-3 w-full justify-end">
|
<div v-if="isTranslations" class="flex gap-3 w-full justify-end">
|
||||||
<UButton @click="() => {
|
<UButton @click="() => {
|
||||||
toLangId = settingStore.shopDefaultLanguage
|
handleCancel()
|
||||||
isTranslations = false
|
isTranslations = false
|
||||||
}" color="info" variant="outline">
|
}" color="info" variant="outline">
|
||||||
Cancel and back to Polish
|
Cancel
|
||||||
</UButton>
|
</UButton>
|
||||||
<UButton color="info" @click="productStore.saveProductDescription(productID, toLangId)">
|
<UButton color="info" @click="async () => {
|
||||||
Save translations
|
await productStore.saveProductDescription(productID, toLangId)
|
||||||
|
isTranslations = false
|
||||||
|
}">
|
||||||
|
Save
|
||||||
</UButton>
|
</UButton>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -65,45 +68,41 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-else-if="productStore.productDescription" class="space-y-7.5">
|
<div v-else-if="productStore.productDescription" class="space-y-7.5">
|
||||||
<div class="flex gap-6 h-full w-full">
|
<div class="grid grid-cols-1 md:grid-cols-6 h-full w-full gap-6">
|
||||||
<div
|
<img class="md:col-span-2 rounded-md" :src="productStore.productDescription.image_link"
|
||||||
class="min-w-96 min-h-96 border border-(--border-light) dark:bg-gray-700 rounded-lg flex items-center justify-center">
|
:alt="productStore.productDescription.name" @onError="(e: Event) => {
|
||||||
<span class="text-gray-500 dark:text-gray-400">Product Image</span>
|
const target = e.target as HTMLImageElement
|
||||||
</div>
|
target.src = errorImg
|
||||||
<div class="flex flex-col justify-between min-h-full w-full">
|
}">
|
||||||
<div>
|
<div class="md:col-span-4 flex flex-col justify-between min-h-full w-full space-y-7.5">
|
||||||
|
<div class="space-y-7.5">
|
||||||
<div>
|
<div>
|
||||||
<p v-if="!isTranslations" class="text-[25px] font-bold text-black dark:text-white">
|
<p v-if="!isTranslations" class="text-[25px] font-bold text-black dark:text-white">
|
||||||
{{ productStore.productDescription.name || 'Product Name' }}
|
{{ productStore.productDescription.name }}
|
||||||
</p>
|
</p>
|
||||||
<div class="" v-if="isTranslations">
|
<div class="" v-if="isTranslations">
|
||||||
<p>Title:</p>
|
<p>Title:</p>
|
||||||
<UEditor v-slot="{ editor }" v-model="productStore.productDescription.name" content-type="html"
|
<UTextarea :rows="1" v-model="productStore.productDescription.name" autoresize :ui="{
|
||||||
:ui="{ base: 'p-8 sm:px-16', root : 'pt-2' }" class="min-w-full border rounded-md border-(--border-light)"
|
root: 'w-full bg-inherit!',
|
||||||
placeholder="Write there ...">
|
base: 'text-2xl! bg-inherit!',
|
||||||
<UEditorToolbar :editor="editor" :items="toolbarItems" class="sm:px-8">
|
}" />
|
||||||
<template #link>
|
|
||||||
<EditorLinkPopover :editor="editor" auto-open />
|
|
||||||
</template>
|
|
||||||
</UEditorToolbar>
|
|
||||||
</UEditor>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="" v-if="isTranslations">
|
||||||
|
<p>Link rewrite:</p>
|
||||||
|
<UTextarea :rows="1" v-model="productStore.productDescription.link_rewrite" autoresize :ui="{
|
||||||
|
root: 'w-full',
|
||||||
|
base: 'bg-inherit!',
|
||||||
|
}" />
|
||||||
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<p v-if="!isTranslations" v-html="productStore.productDescription.description_short"
|
<p v-if="!isTranslations" v-html="productStore.productDescription.description_short"
|
||||||
class="text-black dark:text-white"></p>
|
class="text-black dark:text-white"></p>
|
||||||
<div class="" v-if="isTranslations">
|
<div class="" v-if="isTranslations">
|
||||||
<p>Short description:</p>
|
<p>Short description:</p>
|
||||||
<UEditor v-slot="{ editor }" v-model="productStore.productDescription.description_short"
|
<ProductEditor v-model="productStore.productDescription.description_short" />
|
||||||
content-type="html" :ui="{ base: 'p-8 sm:px-16' }"
|
|
||||||
class="w-full border rounded-md border-(--border-light)" placeholder="Write there ...">
|
|
||||||
<UEditorToolbar :editor="editor" :items="toolbarItems" class="sm:px-8">
|
|
||||||
<template #link>
|
|
||||||
<EditorLinkPopover :editor="editor" auto-open />
|
|
||||||
</template>
|
|
||||||
</UEditorToolbar>
|
|
||||||
</UEditor>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -132,64 +131,13 @@
|
|||||||
root: 'items-start!'
|
root: 'items-start!'
|
||||||
}">
|
}">
|
||||||
<template #description>
|
<template #description>
|
||||||
<!-- <div v-if="!isTranslations" class="flex justify-end items-center gap-3 mb-4">
|
<ProductEditor v-if="isTranslations" v-model="productStore.productDescription.description" />
|
||||||
<UButton v-if="!isEditing" @click="activeTab === 'usage' ? enableEdit() : enableDescriptionEdit()"
|
|
||||||
class="flex items-center gap-2 m-2 cursor-pointer bg-(--accent-blue-light)! dark:bg-(--accent-blue-dark)!">
|
|
||||||
<p class="text-white">Change Text</p>
|
|
||||||
<UIcon name="material-symbols-light:stylus-note-sharp" class="text-[30px] text-white!" />
|
|
||||||
</UButton>
|
|
||||||
<UButton v-if="isEditing" @click="activeTab === 'usage' ? saveText : saveDescription" color="neutral"
|
|
||||||
variant="outline" class="p-2.5 cursor-pointer">
|
|
||||||
<p class="dark:text-white text-black">Save the edited text</p>
|
|
||||||
</UButton>
|
|
||||||
<UButton v-if="isEditing" @click="activeTab === 'usage' ? cancelEdit : cancelDescriptionEdit"
|
|
||||||
color="neutral" variant="outline" class="p-2.5 cursor-pointer">
|
|
||||||
Cancel
|
|
||||||
</UButton>
|
|
||||||
</div> -->
|
|
||||||
<UEditor v-if="isTranslations" v-slot="{ editor }" v-model="productStore.productDescription.description"
|
|
||||||
content-type="html" :ui="{ base: 'p-8 sm:px-16' }" class="w-full min-h-74" placeholder="Write there ...">
|
|
||||||
<UEditorToolbar :editor="editor" :items="toolbarItems" class="sm:px-8">
|
|
||||||
<template #link>
|
|
||||||
<EditorLinkPopover :editor="editor" auto-open />
|
|
||||||
</template>
|
|
||||||
</UEditorToolbar>
|
|
||||||
</UEditor>
|
|
||||||
<p v-else v-html="productStore.productDescription.description" class="text-black dark:text-white"></p>
|
<p v-else v-html="productStore.productDescription.description" class="text-black dark:text-white"></p>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #usage>
|
<template #usage>
|
||||||
<div class="px-7">
|
<ProductEditor v-if="isTranslations" v-model="productStore.productDescription.usage" />
|
||||||
<div class="flex justify-end items-center gap-3 mb-4">
|
|
||||||
<UButton v-if="!isEditing" @click="enableEdit"
|
|
||||||
class="flex items-center gap-2 m-2 cursor-pointer bg-(--accent-blue-light)! dark:bg-(--accent-blue-dark)!">
|
|
||||||
<p class="text-(--text-sky-light) dark:text-(--text-sky-dark)">Change Text</p>
|
|
||||||
<UIcon name="material-symbols-light:stylus-note-sharp"
|
|
||||||
class="text-[30px] text-(--text-sky-light) dark:text-(--text-sky-dark)" />
|
|
||||||
</UButton>
|
|
||||||
|
|
||||||
<UButton v-if="isEditing" @click="saveText" color="neutral" variant="outline"
|
|
||||||
class="p-2.5 cursor-pointer">
|
|
||||||
<p class="dark:text-white text-black">Save the edited text</p>
|
|
||||||
</UButton>
|
|
||||||
|
|
||||||
<UButton v-if="isEditing" @click="cancelEdit" color="neutral" variant="outline"
|
|
||||||
class="p-2.5 cursor-pointer">
|
|
||||||
Cancel
|
|
||||||
</UButton>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<UEditor v-if="isTranslations" v-slot="{ editor }" v-model="productStore.productDescription.usage"
|
|
||||||
content-type="html" :ui="{ base: 'p-8 sm:px-16' }" class="w-full min-h-74"
|
|
||||||
placeholder="Write there ...">
|
|
||||||
<UEditorToolbar :editor="editor" :items="toolbarItems" class="sm:px-8">
|
|
||||||
<template #link>
|
|
||||||
<EditorLinkPopover :editor="editor" auto-open />
|
|
||||||
</template>
|
|
||||||
</UEditorToolbar>
|
|
||||||
</UEditor>
|
|
||||||
<p v-else v-html="productStore.productDescription.usage" class="text-black dark:text-white"></p>
|
<p v-else v-html="productStore.productDescription.usage" class="text-black dark:text-white"></p>
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
</UTabs>
|
</UTabs>
|
||||||
</div>
|
</div>
|
||||||
@@ -198,147 +146,31 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useEditable } from '@/composable/useConteditable';
|
|
||||||
import Default from '@/layouts/default.vue';
|
import Default from '@/layouts/default.vue';
|
||||||
import { langs } from '@/router/langs';
|
import { langs } from '@/router/langs';
|
||||||
import { useProductStore } from '@/stores/product';
|
import { useProductStore } from '@/stores/product';
|
||||||
import { useSettingsStore } from '@/stores/settings';
|
import { useSettingsStore } from '@/stores/settings';
|
||||||
import type { EditorToolbarItem } from '@nuxt/ui';
|
import { computed, onMounted, ref, watch } from 'vue';
|
||||||
import { onMounted, ref, watch } from 'vue';
|
|
||||||
import { useRoute, useRouter } from 'vue-router';
|
import { useRoute, useRouter } from 'vue-router';
|
||||||
|
import ProductEditor from '../inner/ProductEditor.vue';
|
||||||
const toolbarItems: EditorToolbarItem[][] = [
|
import errorImg from '@/assets/error.svg'
|
||||||
// History controls
|
|
||||||
[{
|
|
||||||
kind: 'undo',
|
|
||||||
icon: 'i-lucide-undo',
|
|
||||||
tooltip: { text: 'Undo' }
|
|
||||||
}, {
|
|
||||||
kind: 'redo',
|
|
||||||
icon: 'i-lucide-redo',
|
|
||||||
tooltip: { text: 'Redo' }
|
|
||||||
}],
|
|
||||||
// Block types
|
|
||||||
[{
|
|
||||||
icon: 'i-lucide-heading',
|
|
||||||
tooltip: { text: 'Headings' },
|
|
||||||
content: {
|
|
||||||
align: 'start'
|
|
||||||
},
|
|
||||||
items: [{
|
|
||||||
kind: 'heading',
|
|
||||||
level: 1,
|
|
||||||
icon: 'i-lucide-heading-1',
|
|
||||||
label: 'Heading 1'
|
|
||||||
}, {
|
|
||||||
kind: 'heading',
|
|
||||||
level: 2,
|
|
||||||
icon: 'i-lucide-heading-2',
|
|
||||||
label: 'Heading 2'
|
|
||||||
}, {
|
|
||||||
kind: 'heading',
|
|
||||||
level: 3,
|
|
||||||
icon: 'i-lucide-heading-3',
|
|
||||||
label: 'Heading 3'
|
|
||||||
}, {
|
|
||||||
kind: 'heading',
|
|
||||||
level: 4,
|
|
||||||
icon: 'i-lucide-heading-4',
|
|
||||||
label: 'Heading 4'
|
|
||||||
}]
|
|
||||||
}, {
|
|
||||||
icon: 'i-lucide-list',
|
|
||||||
tooltip: { text: 'Lists' },
|
|
||||||
content: {
|
|
||||||
align: 'start'
|
|
||||||
},
|
|
||||||
items: [{
|
|
||||||
kind: 'bulletList',
|
|
||||||
icon: 'i-lucide-list',
|
|
||||||
label: 'Bullet List'
|
|
||||||
}, {
|
|
||||||
kind: 'orderedList',
|
|
||||||
icon: 'i-lucide-list-ordered',
|
|
||||||
label: 'Ordered List'
|
|
||||||
}]
|
|
||||||
}, {
|
|
||||||
kind: 'blockquote',
|
|
||||||
icon: 'i-lucide-text-quote',
|
|
||||||
tooltip: { text: 'Blockquote' }
|
|
||||||
}, {
|
|
||||||
kind: 'codeBlock',
|
|
||||||
icon: 'i-lucide-square-code',
|
|
||||||
tooltip: { text: 'Code Block' }
|
|
||||||
}, {
|
|
||||||
kind: 'horizontalRule',
|
|
||||||
icon: 'i-lucide-separator-horizontal',
|
|
||||||
tooltip: { text: 'Horizontal Rule' }
|
|
||||||
}],
|
|
||||||
// Text formatting
|
|
||||||
[{
|
|
||||||
kind: 'mark',
|
|
||||||
mark: 'bold',
|
|
||||||
icon: 'i-lucide-bold',
|
|
||||||
tooltip: { text: 'Bold' }
|
|
||||||
}, {
|
|
||||||
kind: 'mark',
|
|
||||||
mark: 'italic',
|
|
||||||
icon: 'i-lucide-italic',
|
|
||||||
tooltip: { text: 'Italic' }
|
|
||||||
}, {
|
|
||||||
kind: 'mark',
|
|
||||||
mark: 'underline',
|
|
||||||
icon: 'i-lucide-underline',
|
|
||||||
tooltip: { text: 'Underline' }
|
|
||||||
}, {
|
|
||||||
kind: 'mark',
|
|
||||||
mark: 'strike',
|
|
||||||
icon: 'i-lucide-strikethrough',
|
|
||||||
tooltip: { text: 'Strikethrough' }
|
|
||||||
}, {
|
|
||||||
kind: 'mark',
|
|
||||||
mark: 'code',
|
|
||||||
icon: 'i-lucide-code',
|
|
||||||
tooltip: { text: 'Code' }
|
|
||||||
}],
|
|
||||||
// Link
|
|
||||||
[{
|
|
||||||
kind: 'link',
|
|
||||||
icon: 'i-lucide-link',
|
|
||||||
tooltip: { text: 'Link' }
|
|
||||||
}],
|
|
||||||
// Text alignment
|
|
||||||
[{
|
|
||||||
icon: 'i-lucide-align-justify',
|
|
||||||
tooltip: { text: 'Text Align' },
|
|
||||||
content: {
|
|
||||||
align: 'end'
|
|
||||||
},
|
|
||||||
items: [{
|
|
||||||
kind: 'textAlign',
|
|
||||||
align: 'left',
|
|
||||||
icon: 'i-lucide-align-left',
|
|
||||||
label: 'Align Left'
|
|
||||||
}, {
|
|
||||||
kind: 'textAlign',
|
|
||||||
align: 'center',
|
|
||||||
icon: 'i-lucide-align-center',
|
|
||||||
label: 'Align Center'
|
|
||||||
}, {
|
|
||||||
kind: 'textAlign',
|
|
||||||
align: 'right',
|
|
||||||
icon: 'i-lucide-align-right',
|
|
||||||
label: 'Align Right'
|
|
||||||
}, {
|
|
||||||
kind: 'textAlign',
|
|
||||||
align: 'justify',
|
|
||||||
icon: 'i-lucide-align-justify',
|
|
||||||
label: 'Align Justify'
|
|
||||||
}]
|
|
||||||
}]
|
|
||||||
]
|
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
const route = useRoute()
|
||||||
|
const settingStore = useSettingsStore()
|
||||||
|
const productStore = useProductStore()
|
||||||
|
|
||||||
|
const toLangId = ref<number>(settingStore.shopDefaultLanguage)
|
||||||
|
const productID = ref<number>(0)
|
||||||
|
const translating = ref(false)
|
||||||
|
const isEditing = ref(false)
|
||||||
|
const isTranslations = ref(false)
|
||||||
|
|
||||||
|
const selectedLangName = computed(() => {
|
||||||
|
return langs.find(l => l.id === toLangId.value)?.name || 'Select language'
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
function backFromProduct() {
|
function backFromProduct() {
|
||||||
let path = localStorage.getItem('back_from_product')
|
let path = localStorage.getItem('back_from_product')
|
||||||
|
|
||||||
@@ -353,23 +185,11 @@ function backFromProduct() {
|
|||||||
localStorage.removeItem('back_from_product')
|
localStorage.removeItem('back_from_product')
|
||||||
} else {
|
} else {
|
||||||
router.push({
|
router.push({
|
||||||
name: 'customer-products',
|
name: 'admin-products',
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const route = useRoute()
|
|
||||||
const settingStore = useSettingsStore()
|
|
||||||
const productStore = useProductStore()
|
|
||||||
const isTranslations = ref(false)
|
|
||||||
|
|
||||||
const toLangId = ref<number>(settingStore.shopDefaultLanguage)
|
|
||||||
const productID = ref<number>(0)
|
|
||||||
const translating = ref(false)
|
|
||||||
|
|
||||||
const activeTab = ref('')
|
|
||||||
const isEditing = ref(false)
|
|
||||||
|
|
||||||
const translateToSelectedLanguage = async () => {
|
const translateToSelectedLanguage = async () => {
|
||||||
if (toLangId.value && productID.value) {
|
if (toLangId.value && productID.value) {
|
||||||
translating.value = true
|
translating.value = true
|
||||||
@@ -389,11 +209,16 @@ const fetchForLanguage = async (langId: number | null) => {
|
|||||||
await productStore.getProductDescription(langId, productID.value)
|
await productStore.getProductDescription(langId, productID.value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
const handleCancel = async () => {
|
||||||
|
await productStore.getProductDescription(toLangId.value, productID.value)
|
||||||
|
isTranslations.value = false
|
||||||
|
}
|
||||||
|
|
||||||
watch(toLangId, () => {
|
watch(toLangId, (lang) => {
|
||||||
fetchForLanguage(toLangId.value)
|
fetchForLanguage(lang)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
const id = route.params.product_id
|
const id = route.params.product_id
|
||||||
if (id) {
|
if (id) {
|
||||||
|
|||||||
160
bo/src/components/inner/ProductEditor.vue
Normal file
160
bo/src/components/inner/ProductEditor.vue
Normal file
@@ -0,0 +1,160 @@
|
|||||||
|
<template>
|
||||||
|
<UEditor v-slot="{ editor }" v-model="localValue" content-type="html"
|
||||||
|
:ui="{ base: 'p-8 sm:px-16', root: 'p-2' }"
|
||||||
|
class="min-w-full border rounded-md border-(--border-light)" placeholder="Write there ...">
|
||||||
|
<UEditorToolbar :editor="editor" :items="toolbarItems" class="sm:px-8 flex-wrap!">
|
||||||
|
<template #link>
|
||||||
|
<EditorLinkPopover :editor="editor" auto-open />
|
||||||
|
</template>
|
||||||
|
</UEditorToolbar>
|
||||||
|
</UEditor>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import type { EditorToolbarItem } from '@nuxt/ui';
|
||||||
|
import { computed } from 'vue';
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
modelValue: string
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'update:modelValue', value: string): void
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const localValue = computed({
|
||||||
|
get: () => props.modelValue,
|
||||||
|
set: (value: string) => emit('update:modelValue', value)
|
||||||
|
})
|
||||||
|
|
||||||
|
const toolbarItems: EditorToolbarItem[][] = [
|
||||||
|
// History controls
|
||||||
|
[{
|
||||||
|
kind: 'undo',
|
||||||
|
icon: 'i-lucide-undo',
|
||||||
|
tooltip: { text: 'Undo' }
|
||||||
|
}, {
|
||||||
|
kind: 'redo',
|
||||||
|
icon: 'i-lucide-redo',
|
||||||
|
tooltip: { text: 'Redo' }
|
||||||
|
}],
|
||||||
|
// Block types
|
||||||
|
[{
|
||||||
|
icon: 'i-lucide-heading',
|
||||||
|
tooltip: { text: 'Headings' },
|
||||||
|
content: {
|
||||||
|
align: 'start'
|
||||||
|
},
|
||||||
|
items: [{
|
||||||
|
kind: 'heading',
|
||||||
|
level: 1,
|
||||||
|
icon: 'i-lucide-heading-1',
|
||||||
|
label: 'Heading 1'
|
||||||
|
}, {
|
||||||
|
kind: 'heading',
|
||||||
|
level: 2,
|
||||||
|
icon: 'i-lucide-heading-2',
|
||||||
|
label: 'Heading 2'
|
||||||
|
}, {
|
||||||
|
kind: 'heading',
|
||||||
|
level: 3,
|
||||||
|
icon: 'i-lucide-heading-3',
|
||||||
|
label: 'Heading 3'
|
||||||
|
}, {
|
||||||
|
kind: 'heading',
|
||||||
|
level: 4,
|
||||||
|
icon: 'i-lucide-heading-4',
|
||||||
|
label: 'Heading 4'
|
||||||
|
}]
|
||||||
|
}, {
|
||||||
|
icon: 'i-lucide-list',
|
||||||
|
tooltip: { text: 'Lists' },
|
||||||
|
content: {
|
||||||
|
align: 'start'
|
||||||
|
},
|
||||||
|
items: [{
|
||||||
|
kind: 'bulletList',
|
||||||
|
icon: 'i-lucide-list',
|
||||||
|
label: 'Bullet List'
|
||||||
|
}, {
|
||||||
|
kind: 'orderedList',
|
||||||
|
icon: 'i-lucide-list-ordered',
|
||||||
|
label: 'Ordered List'
|
||||||
|
}]
|
||||||
|
}, {
|
||||||
|
kind: 'blockquote',
|
||||||
|
icon: 'i-lucide-text-quote',
|
||||||
|
tooltip: { text: 'Blockquote' }
|
||||||
|
}, {
|
||||||
|
kind: 'codeBlock',
|
||||||
|
icon: 'i-lucide-square-code',
|
||||||
|
tooltip: { text: 'Code Block' }
|
||||||
|
}, {
|
||||||
|
kind: 'horizontalRule',
|
||||||
|
icon: 'i-lucide-separator-horizontal',
|
||||||
|
tooltip: { text: 'Horizontal Rule' }
|
||||||
|
}],
|
||||||
|
// Text formatting
|
||||||
|
[{
|
||||||
|
kind: 'mark',
|
||||||
|
mark: 'bold',
|
||||||
|
icon: 'i-lucide-bold',
|
||||||
|
tooltip: { text: 'Bold' }
|
||||||
|
}, {
|
||||||
|
kind: 'mark',
|
||||||
|
mark: 'italic',
|
||||||
|
icon: 'i-lucide-italic',
|
||||||
|
tooltip: { text: 'Italic' }
|
||||||
|
}, {
|
||||||
|
kind: 'mark',
|
||||||
|
mark: 'underline',
|
||||||
|
icon: 'i-lucide-underline',
|
||||||
|
tooltip: { text: 'Underline' }
|
||||||
|
}, {
|
||||||
|
kind: 'mark',
|
||||||
|
mark: 'strike',
|
||||||
|
icon: 'i-lucide-strikethrough',
|
||||||
|
tooltip: { text: 'Strikethrough' }
|
||||||
|
}, {
|
||||||
|
kind: 'mark',
|
||||||
|
mark: 'code',
|
||||||
|
icon: 'i-lucide-code',
|
||||||
|
tooltip: { text: 'Code' }
|
||||||
|
}],
|
||||||
|
// Link
|
||||||
|
[{
|
||||||
|
kind: 'link',
|
||||||
|
icon: 'i-lucide-link',
|
||||||
|
tooltip: { text: 'Link' }
|
||||||
|
}],
|
||||||
|
// Text alignment
|
||||||
|
[{
|
||||||
|
icon: 'i-lucide-align-justify',
|
||||||
|
tooltip: { text: 'Text Align' },
|
||||||
|
content: {
|
||||||
|
align: 'end'
|
||||||
|
},
|
||||||
|
items: [{
|
||||||
|
kind: 'textAlign',
|
||||||
|
align: 'left',
|
||||||
|
icon: 'i-lucide-align-left',
|
||||||
|
label: 'Align Left'
|
||||||
|
}, {
|
||||||
|
kind: 'textAlign',
|
||||||
|
align: 'center',
|
||||||
|
icon: 'i-lucide-align-center',
|
||||||
|
label: 'Align Center'
|
||||||
|
}, {
|
||||||
|
kind: 'textAlign',
|
||||||
|
align: 'right',
|
||||||
|
icon: 'i-lucide-align-right',
|
||||||
|
label: 'Align Right'
|
||||||
|
}, {
|
||||||
|
kind: 'textAlign',
|
||||||
|
align: 'justify',
|
||||||
|
icon: 'i-lucide-align-justify',
|
||||||
|
label: 'Align Justify'
|
||||||
|
}]
|
||||||
|
}]
|
||||||
|
]
|
||||||
|
</script>
|
||||||
@@ -50,7 +50,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex-1 p-4 bg-slate-50 h-svh">
|
<div class="flex-1 p-4 bg-slate-50">
|
||||||
<slot />
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ export const useProductStore = defineStore('product', () => {
|
|||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const error = ref<string | null>(null)
|
const error = ref<string | null>(null)
|
||||||
const productDescription = ref()
|
const productDescription = ref()
|
||||||
|
const copyProductDescription = ref()
|
||||||
|
|
||||||
async function getProductDescription(langId: number | null, productID: number) {
|
async function getProductDescription(langId: number | null, productID: number) {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
@@ -36,8 +37,8 @@ export const useProductStore = defineStore('product', () => {
|
|||||||
const response = await useFetchJson<ProductDescription>(
|
const response = await useFetchJson<ProductDescription>(
|
||||||
`/api/v1/restricted/product-translation/get-product-description?productID=${productID}&productLangID=${langId ? langId : settingStore.shopDefaultLanguage}`
|
`/api/v1/restricted/product-translation/get-product-description?productID=${productID}&productLangID=${langId ? langId : settingStore.shopDefaultLanguage}`
|
||||||
)
|
)
|
||||||
productDescription.value = response.items
|
productDescription.value = structuredClone(response.items)
|
||||||
|
copyProductDescription.value = structuredClone(response.items)
|
||||||
} catch (e: unknown) {
|
} catch (e: unknown) {
|
||||||
error.value = e instanceof Error ? e.message : 'Failed to load product description'
|
error.value = e instanceof Error ? e.message : 'Failed to load product description'
|
||||||
} finally {
|
} finally {
|
||||||
@@ -45,7 +46,6 @@ export const useProductStore = defineStore('product', () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const translat = ref()
|
|
||||||
const settingStore = useSettingsStore()
|
const settingStore = useSettingsStore()
|
||||||
async function translateProductDescription(productID: number, toLangId: number, model: string = 'Google') {
|
async function translateProductDescription(productID: number, toLangId: number, model: string = 'Google') {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
@@ -87,6 +87,8 @@ export const useProductStore = defineStore('product', () => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
await getProductDescription(langId, productID)
|
||||||
return data
|
return data
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e)
|
console.error(e)
|
||||||
@@ -97,7 +99,7 @@ export const useProductStore = defineStore('product', () => {
|
|||||||
productDescription,
|
productDescription,
|
||||||
loading,
|
loading,
|
||||||
error,
|
error,
|
||||||
translat,
|
copyProductDescription,
|
||||||
translateProductDescription,
|
translateProductDescription,
|
||||||
getProductDescription,
|
getProductDescription,
|
||||||
saveProductDescription
|
saveProductDescription
|
||||||
|
|||||||
2
bo/src/types/product.d.ts
vendored
2
bo/src/types/product.d.ts
vendored
@@ -7,6 +7,8 @@ export interface ProductDescription {
|
|||||||
available_now: string
|
available_now: string
|
||||||
delivery_in_stock?: string
|
delivery_in_stock?: string
|
||||||
usage: string
|
usage: string
|
||||||
|
image_link: string
|
||||||
|
link_rewrite: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Product {
|
export interface Product {
|
||||||
|
|||||||
Reference in New Issue
Block a user