Merge branch 'front-styles' of ssh://git.ma-al.com:8822/goc_daniel/b2b into translate
This commit is contained in:
@@ -52,10 +52,10 @@ func (r *ProductDescriptionRepo) GetProductDescription(productID uint, productid
|
|||||||
|
|
||||||
// If it doesn't exist, returns an error.
|
// If it doesn't exist, returns an error.
|
||||||
func (r *ProductDescriptionRepo) CreateIfDoesNotExist(productID uint, productid_lang uint) error {
|
func (r *ProductDescriptionRepo) CreateIfDoesNotExist(productID uint, productid_lang uint) error {
|
||||||
record := model.ProductDescription{
|
record := dbmodel.PsProductLang{
|
||||||
ProductID: productID,
|
IDProduct: int32(productID),
|
||||||
ShopID: constdata.SHOP_ID,
|
IDShop: int32(constdata.SHOP_ID),
|
||||||
LangID: productid_lang,
|
IDLang: int32(productid_lang),
|
||||||
}
|
}
|
||||||
|
|
||||||
err := db.Get().
|
err := db.Get().
|
||||||
|
|||||||
@@ -45,7 +45,7 @@
|
|||||||
}" color="primary" class="text-white bg-(--accent-blue-light) dark:bg-(--accent-blue-dark) px-12!">
|
}" color="primary" class="text-white bg-(--accent-blue-light) dark:bg-(--accent-blue-dark) px-12!">
|
||||||
Cancel
|
Cancel
|
||||||
</UButton>
|
</UButton>
|
||||||
<UButton color="primary" @click="saveDescription"
|
<UButton color="primary" @click="productStore.saveProductDescription(productID, toLangId)"
|
||||||
class="text-white bg-(--accent-blue-light) dark:bg-(--accent-blue-dark) px-12!">
|
class="text-white bg-(--accent-blue-light) dark:bg-(--accent-blue-dark) px-12!">
|
||||||
Save
|
Save
|
||||||
</UButton>
|
</UButton>
|
||||||
|
|||||||
@@ -1,34 +1,34 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="flex flex-col">
|
<USelectMenu v-model="country" :items="countries" value-key="id"
|
||||||
<p class="text-sm">Country/Currency:</p>
|
class="w-48 bg-(--main-light) dark:bg-(--black) rounded-md shadow-sm" :searchInput="false">
|
||||||
<USelectMenu v-model="country" :items="countries"
|
<template #default>
|
||||||
class="w-44 bg-(--main-light) dark:bg-(--black) rounded-md hover:none! text-sm!" valueKey="id"
|
<div class="flex flex-col items-start leading-tight">
|
||||||
:searchInput="false">
|
<span class="text-xs text-gray-400">
|
||||||
<template #default="{ modelValue }">
|
Country/Currency
|
||||||
<div class="flex items-center gap-1">
|
</span>
|
||||||
<span class="font-medium dark:text-white text-black whitespace-nowrap">{{ modelValue.name }} / {{
|
<span class="font-medium dark:text-white text-black">
|
||||||
currentCountry?.ps_currency.iso_code }}</span>
|
{{ country?.name }} / {{ country?.ps_currency.iso_code }}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #item-leading="{ item }">
|
<template #item-leading="{ item }">
|
||||||
<div class="flex items-center rounded-md cursor-pointer transition-colors">
|
<div class="flex items-center gap-2 cursor-pointer">
|
||||||
<span class="ml-2 dark:text-white text-black font-medium">{{ item.name }}</span>
|
<span class="text-lg">{{ item.flag }}</span>
|
||||||
|
<span class="font-medium dark:text-white text-black">
|
||||||
|
{{ item.name }}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</USelectMenu>
|
</USelectMenu>
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { countries, currentCountry, switchLocalization } from '@/router/langs'
|
import { countries, currentCountry, switchLocalization } from '@/router/langs'
|
||||||
import { useRouter, useRoute } from 'vue-router'
|
|
||||||
import { useCookie } from '@/composable/useCookie'
|
import { useCookie } from '@/composable/useCookie'
|
||||||
import { computed, watch } from 'vue'
|
import { computed, watch } from 'vue'
|
||||||
const router = useRouter()
|
|
||||||
const route = useRoute()
|
|
||||||
|
|
||||||
const cookie = useCookie()
|
const cookie = useCookie()
|
||||||
|
|
||||||
const country = computed({
|
const country = computed({
|
||||||
get() {
|
get() {
|
||||||
return currentCountry.value
|
return currentCountry.value
|
||||||
|
|||||||
@@ -1,24 +1,30 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="flex flex-col">
|
<USelectMenu v-model="locale" :items="langs" value-key="iso_code"
|
||||||
<p class="text-sm">Language:</p>
|
class="w-48 bg-(--main-light) dark:bg-(--black) rounded-md shadow-sm" :searchInput="false">
|
||||||
<USelectMenu v-model="locale" :items="langs"
|
<template #default>
|
||||||
class="w-40 bg-(--main-light) dark:bg-(--black) rounded-md shadow-sm hover:none!" valueKey="iso_code"
|
<div class="flex items-center gap-2">
|
||||||
:searchInput="false">
|
<!-- <span class="text-lg">{{ selectedLang?.flag }}</span> -->
|
||||||
<template #default="{ modelValue }">
|
|
||||||
<div class="flex items-center gap-1">
|
<div class="flex flex-col leading-tight items-start">
|
||||||
<!-- <span class="text-md dark:text-white text-black">{{langs.find(x => x.iso_code == modelValue)?.flag}}</span> -->
|
<span class="text-xs text-gray-400">
|
||||||
<span class="font-medium dark:text-white text-black">{{langs.find(x => x.iso_code ==
|
Language
|
||||||
modelValue)?.name}}</span>
|
</span>
|
||||||
|
<span class="font-medium dark:text-white text-black">
|
||||||
|
{{ selectedLang?.name || 'Select language' }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #item-leading="{ item }">
|
<template #item-leading="{ item }">
|
||||||
<div class="flex items-center rounded-md cursor-pointer transition-colors">
|
<div class="flex items-center gap-2">
|
||||||
<!-- <span class="text-md ">{{ item.flag }}</span> -->
|
<span class="text-lg">{{ item.flag }}</span>
|
||||||
<span class="ml-2 dark:text-white text-black font-medium">{{ item.name }}</span>
|
<span class="font-medium dark:text-white text-black">
|
||||||
|
{{ item.name }}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</USelectMenu>
|
</USelectMenu>
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
@@ -31,6 +37,10 @@ import { i18n } from '@/plugins/02_i18n'
|
|||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
|
|
||||||
|
const selectedLang = computed(() =>
|
||||||
|
langs.find(item => item.iso_code === locale.value)
|
||||||
|
)
|
||||||
|
|
||||||
const cookie = useCookie()
|
const cookie = useCookie()
|
||||||
const locale = computed({
|
const locale = computed({
|
||||||
get() {
|
get() {
|
||||||
|
|||||||
@@ -1,19 +1,61 @@
|
|||||||
<!-- <script setup lang="ts">
|
|
||||||
import TopBar from '@/components/TopBar.vue';
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="h-screen grid grid-rows-[auto_1fr_auto]">
|
<div class="flex flex-1">
|
||||||
<main class="pt-20 pb-10">
|
<USidebar v-model:open="open" collapsible="icon" rail :ui="{
|
||||||
<TopBar />
|
container: 'h-full',
|
||||||
<div class=" px-4">
|
inner: 'bg-elevated/25 divide-transparent',
|
||||||
|
body: 'py-0'
|
||||||
|
}">
|
||||||
|
<template #header>
|
||||||
|
<UDropdownMenu :items="teamsItems" :content="{ align: 'start', collisionPadding: 12 }"
|
||||||
|
:ui="{ content: 'w-(--reka-dropdown-menu-trigger-width) min-w-48' }">
|
||||||
|
<UButton v-bind="selectedTeam" trailing-icon="i-lucide-chevrons-up-down" color="neutral" variant="ghost"
|
||||||
|
square class="w-full data-[state=open]:bg-elevated overflow-hidden" :ui="{
|
||||||
|
trailingIcon: 'text-dimmed ms-auto'
|
||||||
|
}" />
|
||||||
|
</UDropdownMenu>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #default="{ state }">
|
||||||
|
<UNavigationMenu :key="state" :items="menuItems" orientation="vertical"
|
||||||
|
:ui="{ link: 'p-1.5 overflow-hidden' }" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<UDropdownMenu :items="userItems" :content="{ align: 'center', collisionPadding: 12 }"
|
||||||
|
:ui="{ content: 'w-(--reka-dropdown-menu-trigger-width) min-w-48' }">
|
||||||
|
<UButton v-bind="user" :label="user?.name" trailing-icon="i-lucide-chevrons-up-down" color="neutral"
|
||||||
|
variant="ghost" square class="w-full data-[state=open]:bg-elevated overflow-hidden" :ui="{
|
||||||
|
trailingIcon: 'text-dimmed ms-auto'
|
||||||
|
}" />
|
||||||
|
</UDropdownMenu>
|
||||||
|
</template>
|
||||||
|
</USidebar>
|
||||||
|
|
||||||
|
<div class="flex-1 flex flex-col">
|
||||||
|
<div class="h-(--ui-header-height) shrink-0 flex items-center justify-between px-4 border-b border-default">
|
||||||
|
<UButton icon="i-lucide-panel-left" color="neutral" variant="ghost" aria-label="Toggle sidebar"
|
||||||
|
@click="open = !open" />
|
||||||
|
<div class="flex items-center gap-12">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<CountryCurrencySwitch />
|
||||||
|
<LangSwitch />
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<ThemeSwitch />
|
||||||
|
<button v-if="authStore.isAuthenticated" @click="authStore.logout()"
|
||||||
|
class="px-3 py-1.5 text-sm font-medium text-black dark:text-white hover:text-black dark:hover:text-white hover:bg-gray-100 dark:hover:bg-gray-600 rounded-lg transition-colors border border-(--border-light) dark:border-(--border-dark) whitespace-nowrap">
|
||||||
|
{{ $t('general.logout') }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex-1 p-4">
|
||||||
<slot />
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
</main>
|
|
||||||
</div>
|
</div>
|
||||||
</template> -->
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed, onMounted } from 'vue'
|
import { ref, computed, onMounted } from 'vue'
|
||||||
@@ -23,7 +65,7 @@ import { defineShortcuts, extractShortcuts } from '@nuxt/ui/runtime/composables/
|
|||||||
import { LabelTrans, TopMenuItem } from '@/types'
|
import { LabelTrans, TopMenuItem } from '@/types'
|
||||||
|
|
||||||
const open = ref(true)
|
const open = ref(true)
|
||||||
|
const authStore = useAuthStore()
|
||||||
const colorMode = useColorMode()
|
const colorMode = useColorMode()
|
||||||
|
|
||||||
const teams = ref([
|
const teams = ref([
|
||||||
@@ -113,6 +155,9 @@ function getItems(state: 'collapsed' | 'expanded') {
|
|||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import { currentLang } from '@/router/langs'
|
import { currentLang } from '@/router/langs'
|
||||||
import { useFetchJson } from '@/composable/useFetchJson'
|
import { useFetchJson } from '@/composable/useFetchJson'
|
||||||
|
import { useAuthStore } from '@/stores/auth'
|
||||||
|
import CountryCurrencySwitch from '@/components/inner/CountryCurrencySwitch.vue'
|
||||||
|
import LangSwitch from '@/components/inner/LangSwitch.vue'
|
||||||
|
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
@@ -246,49 +291,3 @@ const userItems = computed<DropdownMenuItem[][]>(() => [
|
|||||||
|
|
||||||
defineShortcuts(extractShortcuts(teamsItems.value))
|
defineShortcuts(extractShortcuts(teamsItems.value))
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
|
||||||
<div class="flex flex-1">
|
|
||||||
<USidebar v-model:open="open" collapsible="icon" rail :ui="{
|
|
||||||
container: 'h-full',
|
|
||||||
inner: 'bg-elevated/25 divide-transparent',
|
|
||||||
body: 'py-0'
|
|
||||||
}">
|
|
||||||
<template #header>
|
|
||||||
<UDropdownMenu :items="teamsItems" :content="{ align: 'start', collisionPadding: 12 }"
|
|
||||||
:ui="{ content: 'w-(--reka-dropdown-menu-trigger-width) min-w-48' }">
|
|
||||||
<UButton v-bind="selectedTeam" trailing-icon="i-lucide-chevrons-up-down" color="neutral" variant="ghost"
|
|
||||||
square class="w-full data-[state=open]:bg-elevated overflow-hidden" :ui="{
|
|
||||||
trailingIcon: 'text-dimmed ms-auto'
|
|
||||||
}" />
|
|
||||||
</UDropdownMenu>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template #default="{ state }">
|
|
||||||
<UNavigationMenu :key="state" :items="menuItems" orientation="vertical"
|
|
||||||
:ui="{ link: 'p-1.5 overflow-hidden' }" />
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template #footer>
|
|
||||||
<UDropdownMenu :items="userItems" :content="{ align: 'center', collisionPadding: 12 }"
|
|
||||||
:ui="{ content: 'w-(--reka-dropdown-menu-trigger-width) min-w-48' }">
|
|
||||||
<UButton v-bind="user" :label="user?.name" trailing-icon="i-lucide-chevrons-up-down" color="neutral"
|
|
||||||
variant="ghost" square class="w-full data-[state=open]:bg-elevated overflow-hidden" :ui="{
|
|
||||||
trailingIcon: 'text-dimmed ms-auto'
|
|
||||||
}" />
|
|
||||||
</UDropdownMenu>
|
|
||||||
</template>
|
|
||||||
</USidebar>
|
|
||||||
|
|
||||||
<div class="flex-1 flex flex-col">
|
|
||||||
<div class="h-(--ui-header-height) shrink-0 flex items-center px-4 border-b border-default">
|
|
||||||
<UButton icon="i-lucide-panel-left" color="neutral" variant="ghost" aria-label="Toggle sidebar"
|
|
||||||
@click="open = !open" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex-1 p-4">
|
|
||||||
<slot />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ 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
|
||||||
@@ -53,6 +54,7 @@ export const useProductStore = defineStore('product', () => {
|
|||||||
try {
|
try {
|
||||||
const response = await useFetchJson<ProductDescription>(`/api/v1/restricted/product-translation/translate-product-description?productID=${productID}&productFromLangID=${settingStore.shopDefaultLanguage}&productToLangID=${toLangId}&model=${model}`)
|
const response = await useFetchJson<ProductDescription>(`/api/v1/restricted/product-translation/translate-product-description?productID=${productID}&productFromLangID=${settingStore.shopDefaultLanguage}&productToLangID=${toLangId}&model=${model}`)
|
||||||
productDescription.value = response.items
|
productDescription.value = response.items
|
||||||
|
saveProductDescription(productID, toLangId)
|
||||||
return response.items
|
return response.items
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
error.value = e?.message || 'Failed to translate product description'
|
error.value = e?.message || 'Failed to translate product description'
|
||||||
@@ -62,16 +64,46 @@ export const useProductStore = defineStore('product', () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function fixHtml(html: string) {
|
||||||
|
return html
|
||||||
|
// 1. fix img
|
||||||
|
.replace(/<img([^>]*?)>/g, '<img$1 />')
|
||||||
|
|
||||||
function stripHtml(html: string) {
|
// 2. escape text only
|
||||||
const div = document.createElement('div')
|
.replace(/>([^<]+)</g, (match, text) => {
|
||||||
div.innerHTML = html
|
const escaped = text
|
||||||
return div.textContent || div.innerText || ''
|
.replace(/&/g, '&')
|
||||||
|
.replace(/"/g, '"')
|
||||||
|
.replace(/'/g, ''')
|
||||||
|
|
||||||
|
return `>${escaped}<`
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async function saveProductDescription(productID?: number, langId?: number) {
|
function fixAll(obj: any): any {
|
||||||
|
if (typeof obj === 'string') {
|
||||||
|
return fixHtml(obj)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(obj)) {
|
||||||
|
return obj.map(fixAll)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof obj === 'object' && obj !== null) {
|
||||||
|
const result: any = {}
|
||||||
|
for (const [key, value] of Object.entries(obj)) {
|
||||||
|
result[key] = fixAll(value)
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
return obj
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveProductDescription(productID?: number, langId?: number | null) {
|
||||||
const id = productID || 1
|
const id = productID || 1
|
||||||
const lang = langId || 1
|
const lang = langId || 1
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const data = await useFetchJson(
|
const data = await useFetchJson(
|
||||||
`/api/v1/restricted/product-translation/save-product-description?productID=${id}&productLangID=${lang}`,
|
`/api/v1/restricted/product-translation/save-product-description?productID=${id}&productLangID=${lang}`,
|
||||||
@@ -81,14 +113,14 @@ export const useProductStore = defineStore('product', () => {
|
|||||||
'Content-Type': 'application/json'
|
'Content-Type': 'application/json'
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
name: stripHtml(productDescription.value?.name || ''),
|
name: productDescription.value?.name || '',
|
||||||
description: stripHtml(productDescription.value?.description || ''),
|
description: productDescription.value?.description || '',
|
||||||
description_short: stripHtml(productDescription.value?.description_short || ''),
|
description_short: productDescription.value?.description_short || '',
|
||||||
meta_title: stripHtml(productDescription.value?.meta_title || ''),
|
meta_title: productDescription.value?.meta_title || '',
|
||||||
meta_description: stripHtml(productDescription.value?.meta_description || ''),
|
meta_description: productDescription.value?.meta_description || '',
|
||||||
available_now: stripHtml(productDescription.value?.available_now || ''),
|
available_now: productDescription.value?.available_now || '',
|
||||||
available_later: stripHtml(productDescription.value?.available_later || ''),
|
available_later: productDescription.value?.available_later || '',
|
||||||
usage: stripHtml(productDescription.value?.usage || ''),
|
usage: productDescription.value?.usage || '',
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -102,6 +134,7 @@ export const useProductStore = defineStore('product', () => {
|
|||||||
productDescription,
|
productDescription,
|
||||||
loading,
|
loading,
|
||||||
error,
|
error,
|
||||||
|
translat,
|
||||||
translateProductDescription,
|
translateProductDescription,
|
||||||
getProductDescription,
|
getProductDescription,
|
||||||
saveProductDescription
|
saveProductDescription
|
||||||
|
|||||||
Reference in New Issue
Block a user