Merge remote-tracking branch 'origin/main' into front-styles

This commit is contained in:
2026-04-03 13:10:05 +02:00
15 changed files with 402 additions and 380 deletions

View File

@@ -18,8 +18,9 @@ type ProductDescription struct {
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"`
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"`
}

View File

@@ -4,6 +4,7 @@ import (
"errors"
"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/model"
"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),
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
if errors.Is(err, gorm.ErrRecordNotFound) {

2
bo/components.d.ts vendored
View File

@@ -34,6 +34,7 @@ declare module 'vue' {
ProductCustomization: typeof import('./src/components/customer/components/ProductCustomization.vue')['default']
ProductDetailView: typeof import('./src/components/admin/ProductDetailView.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']
RouterLink: typeof import('vue-router')['RouterLink']
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']
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']
UTextarea: typeof import('./node_modules/@nuxt/ui/dist/runtime/components/Textarea.vue')['default']
}
}

View File

@@ -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)',
}
},
textarea: {
slots: {
base: 'ring-0! outline-0! rounded-md border border-(--border-light)! dark:border-(--border-dark)! text-base!'
}
}
}
}

View File

@@ -5,6 +5,10 @@ body {
font-family: "Inter", sans-serif;
}
li {
margin-left: 20px
}
.inter {
font-family: "Inter", sans-serif;
}

View File

@@ -4,7 +4,7 @@
<CategoryMenu />
<div class="w-full flex flex-col items-center gap-4">
<UTable :data="productsList" :columns="columns" class="flex-1 w-full" :ui="{
root : 'max-w-100wv overflow-auto!'
root: 'max-w-100wv overflow-auto!'
}" />
<UPagination v-model:page="page" :total="total" :items-per-page="perPage" />
</div>
@@ -153,7 +153,7 @@ async function fetchProductList() {
}
}
function goToProduct(productId: number) {
function goToProduct(productId: number, linkRewrite: string) {
let path = {
name: route.name,
params: route.params,
@@ -162,7 +162,7 @@ function goToProduct(productId: number) {
localStorage.setItem('back_from_product', JSON.stringify(path))
router.push({
name: 'customer-product-details',
params: { product_id: productId }
params: { product_id: productId, link_rewrite: linkRewrite }
})
}
@@ -281,7 +281,7 @@ const columns: TableColumn<Product>[] = [
cell: ({ row }) => {
return h(UButton, {
onClick: () => {
goToProduct(row.original.product_id)
goToProduct(row.original.product_id, row.original.link_rewrite)
},
class: 'cursor-pointer',
color: 'info',

View File

@@ -6,9 +6,8 @@
Back to products</p>
</div>
<div>
<div
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 items-center justify-between">
<div class="gap-4 mb-6 border-b border-(--border-light) dark:border-(--border-dark) py-4">
<div class="flex flex-wrap gap-4 items-center justify-between">
<div class="flex flex-col items-center gap-3" v-if="!isTranslations">
<USelectMenu v-model="toLangId" :items="langs" value-key="id"
class="w-48 bg-(--main-light) dark:bg-(--black) rounded-md shadow-sm" :searchInput="false">
@@ -18,7 +17,7 @@
Selected language
</span>
<span class="font-medium dark:text-white text-black">
{{langs.find(l => l.id === toLangId)?.name || 'Select language'}}
{{ selectedLangName }}
</span>
</div>
</template>
@@ -34,7 +33,8 @@
</USelectMenu>
</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">
<p>Change Text</p>
<UIcon name="material-symbols-light:stylus-note-sharp" class="text-7.5" />
@@ -45,13 +45,16 @@
</div>
<div v-if="isTranslations" class="flex gap-3 w-full justify-end">
<UButton @click="() => {
toLangId = settingStore.shopDefaultLanguage
handleCancel()
isTranslations = false
}" color="info" variant="outline">
Cancel and back to Polish
Cancel
</UButton>
<UButton color="info" @click="productStore.saveProductDescription(productID, toLangId)">
Save translations
<UButton color="info" @click="async () => {
await productStore.saveProductDescription(productID, toLangId)
isTranslations = false
}">
Save
</UButton>
</div>
</div>
@@ -65,62 +68,59 @@
</div>
<div v-else-if="productStore.productDescription" class="space-y-7.5">
<div class="flex gap-6 h-full w-full">
<div
class="min-w-96 min-h-96 border border-(--border-light) dark:bg-gray-700 rounded-lg flex items-center justify-center">
<span class="text-gray-500 dark:text-gray-400">Product Image</span>
</div>
<div class="flex flex-col justify-between min-h-full w-full">
<div>
<div class="grid grid-cols-1 md:grid-cols-6 h-full w-full gap-6">
<img
class="md:col-span-2 rounded-md bg-white rounded-md border border-(--border-light) dark:border-(--border-dark)"
:src="productStore.productDescription.image_link" :alt="productStore.productDescription.name" @error="(e: Event) => {
const target = e.target as HTMLImageElement
target.src = errorImg
}">
<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>
<p v-if="!isTranslations" class="text-[25px] font-bold text-black dark:text-white">
{{ productStore.productDescription.name || 'Product Name' }}
{{ productStore.productDescription.name || 'Product name not provided' }}
</p>
<div class="" v-if="isTranslations">
<p>Title:</p>
<UEditor v-slot="{ editor }" v-model="productStore.productDescription.name" content-type="html"
:ui="{ base: 'p-8 sm:px-16', root : 'pt-2' }" class="min-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>
<UTextarea :rows="1" v-model="productStore.productDescription.name" autoresize :ui="{
root: 'w-full bg-white!',
base: 'text-2xl!',
}" />
</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 bg-white!'
}" />
</div>
<div>
<p v-if="!isTranslations" v-html="productStore.productDescription.description_short"
class="text-black dark:text-white"></p>
<p v-if="!isTranslations"
v-html="productStore.productDescription.description_short || 'No short description available'"
class="text-black dark:text-white" />
<div class="" v-if="isTranslations">
<p>Short description:</p>
<UEditor v-slot="{ editor }" 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>
<ProductEditor v-model="productStore.productDescription.description_short" />
</div>
</div>
</div>
<div class="space-y-[10px]">
<div class="flex items-center gap-1">
<div class="space-y-2.5">
<div v-if="productStore.productDescription.available_now" class="flex items-center gap-1">
<UIcon name="lets-icons:done-ring-round-fill"
class="text-[20px] light:text-(--accent-green-light) dark:text-(--accent-green-dark)" />
<p>
{{ productStore.productDescription.available_now || 'Available now' }}
{{ productStore.productDescription.available_now }}
</p>
</div>
<div class="flex items-center gap-1">
<div v-if="productStore.productDescription.delivery_in_stock" class="flex items-center gap-1">
<UIcon name="marketeq:car-shipping"
class="text-[25px] light:text-(--accent-green-light) dark:text-(--accent-green-dark)" />
<p>
{{ productStore.productDescription.delivery_in_stock || 'Delivery information' }}
{{ productStore.productDescription.delivery_in_stock }}
</p>
</div>
</div>
@@ -132,213 +132,49 @@
root: 'items-start!'
}">
<template #description>
<!-- <div v-if="!isTranslations" class="flex justify-end items-center gap-3 mb-4">
<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>
<ProductEditor v-if="isTranslations" v-model="productStore.productDescription.description" />
<p v-else v-html="productStore.productDescription.description || 'No description available'"
class="text-black dark:text-white" />
</template>
<template #usage>
<div class="px-7">
<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>
</div>
<ProductEditor v-if="isTranslations" v-model="productStore.productDescription.usage" />
<p v-else v-html="productStore.productDescription.usage || 'No usage information available'"
class="text-black dark:text-white" />
</template>
</UTabs>
</div>
<div class=""></div>
</div>
</component>
</template>
<script setup lang="ts">
import { useEditable } from '@/composable/useConteditable';
import Default from '@/layouts/default.vue';
import { langs } from '@/router/langs';
import { useProductStore } from '@/stores/product';
import { useSettingsStore } from '@/stores/settings';
import type { EditorToolbarItem } from '@nuxt/ui';
import { onMounted, ref, watch } from 'vue';
import { computed, onMounted, ref, watch } from 'vue';
import { useRoute, useRouter } from 'vue-router';
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'
}]
}]
]
import ProductEditor from '../inner/ProductEditor.vue';
import errorImg from '@/assets/error.svg'
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() {
let path = localStorage.getItem('back_from_product')
@@ -353,23 +189,11 @@ function backFromProduct() {
localStorage.removeItem('back_from_product')
} else {
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 () => {
if (toLangId.value && productID.value) {
translating.value = true
@@ -389,11 +213,16 @@ const fetchForLanguage = async (langId: number | null) => {
await productStore.getProductDescription(langId, productID.value)
}
}
const handleCancel = async () => {
await productStore.getProductDescription(toLangId.value, productID.value)
isTranslations.value = false
}
watch(toLangId, () => {
fetchForLanguage(toLangId.value)
watch(toLangId, (lang) => {
fetchForLanguage(lang)
})
onMounted(async () => {
const id = route.params.product_id
if (id) {

View File

@@ -7,7 +7,7 @@
Country/Currency
</span>
<span v-if="country" class="font-medium dark:text-white text-black">
{{ country?.name }} / {{ country?.ps_currency }}
{{ country?.name || 'Country' }} / {{ country?.ps_currency || 'Currency' }}
</span>
</div>
</template>

View 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 bg-white! 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>

View File

@@ -50,7 +50,7 @@
</div>
</div>
<div class="flex-1 p-4 bg-slate-50 h-svh">
<div class="flex-1 p-4 bg-slate-50">
<slot />
</div>
</div>
@@ -168,6 +168,7 @@ const menu = ref<TopMenuItem[] | null>(null)
async function getTopMenu() {
try {
const { items } = await useFetchJson<TopMenuItem[]>('/api/v1/restricted/menu/get-top-menu')
menu.value = items
} catch (err) {
console.log(err)

View File

@@ -47,6 +47,9 @@ export async function initCountryCurrency() {
}
defCountry.value = items.find((x) => x.id === defLang.value?.id)
currentCountry.value = idfromcookie ?? defCountry.value
console.log(defCountry.value);
console.log(currentCountry.value);
} catch (error) {
console.error('Failed to fetch languages:', error)
}

View File

@@ -28,6 +28,7 @@ export const useProductStore = defineStore('product', () => {
const loading = ref(false)
const error = ref<string | null>(null)
const productDescription = ref()
const copyProductDescription = ref()
async function getProductDescription(langId: number | null, productID: number) {
loading.value = true
@@ -36,8 +37,8 @@ export const useProductStore = defineStore('product', () => {
const response = await useFetchJson<ProductDescription>(
`/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) {
error.value = e instanceof Error ? e.message : 'Failed to load product description'
} finally {
@@ -45,7 +46,6 @@ export const useProductStore = defineStore('product', () => {
}
}
const translat = ref()
const settingStore = useSettingsStore()
async function translateProductDescription(productID: number, toLangId: number, model: string = 'Google') {
loading.value = true
@@ -87,6 +87,8 @@ export const useProductStore = defineStore('product', () => {
})
}
)
await getProductDescription(langId, productID)
return data
} catch (e) {
console.error(e)
@@ -97,7 +99,7 @@ export const useProductStore = defineStore('product', () => {
productDescription,
loading,
error,
translat,
copyProductDescription,
translateProductDescription,
getProductDescription,
saveProductDescription

View File

@@ -7,6 +7,8 @@ export interface ProductDescription {
available_now: string
delivery_in_stock?: string
usage: string
image_link: string
link_rewrite: string
}
export interface Product {

View File

@@ -10,35 +10,35 @@ CREATE TABLE IF NOT EXISTS b2b_routes (
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
INSERT IGNORE INTO b2b_routes
(name, path, component, meta, active)
VALUES
('root', '', '', '{"trans": "route.root"}', 0),
('home', '', '/views/HomeView.vue', '{"trans": "route.home"}', 1),
('login', 'login', '/views/LoginView.vue', '{"guest":true}', 1),
('register', 'register', '/views/RegisterView.vue', '{"guest":true}', 1),
('password-recovery', 'password-recovery', '/views/PasswordRecoveryView.vue', '{"guest":true}', 1),
('reset-password', 'reset-password', '/views/ResetPasswordView.vue', '{"guest":true}', 1),
('verify-email', 'verify-email', '/views/VerifyEmailView.vue', '{"guest":true}', 1);
-- INSERT INTO `b2b_routes` (`id`, `name`, `path`, `component`, `meta`, `active`) VALUES (1, 'root', '', '', '{"trans": "route.root"}', 0);
-- INSERT INTO `b2b_routes` (`id`, `name`, `path`, `component`, `meta`, `active`) VALUES (2, 'home', '', '/views/HomeView.vue', '{"trans": "route.home"}', 1);
-- INSERT INTO `b2b_routes` (`id`, `name`, `path`, `component`, `meta`, `active`) VALUES (3, 'login', 'login', '/views/LoginView.vue', '{"guest":true}', 1);
-- INSERT INTO `b2b_routes` (`id`, `name`, `path`, `component`, `meta`, `active`) VALUES (4, 'register', 'register', '/views/RegisterView.vue', '{"guest":true}', 1);
-- INSERT INTO `b2b_routes` (`id`, `name`, `path`, `component`, `meta`, `active`) VALUES (5, 'password-recovery', 'password-recovery', '/views/PasswordRecoveryView.vue', '{"guest":true}', 1);
-- INSERT INTO `b2b_routes` (`id`, `name`, `path`, `component`, `meta`, `active`) VALUES (6, 'reset-password', 'reset-password', '/views/ResetPasswordForm.vue', '{"guest":true}', 1);
-- INSERT INTO `b2b_routes` (`id`, `name`, `path`, `component`, `meta`, `active`) VALUES (7, 'verify-email', 'verify-email', '/views/VerifyEmailView.vue', '{"guest":true}', 1);
-- INSERT INTO `b2b_routes` (`id`, `name`, `path`, `component`, `meta`, `active`) VALUES (8, 'category', 'category/:category_id-:link_rewrite', '/views/CategoryView.vue', '{"guest":true}', 1);
-- INSERT INTO `b2b_routes` (`id`, `name`, `path`, `component`, `meta`, `active`) VALUES (9, 'admin-products-category', 'products/:category_id-:link_rewrite', '/components/admin/PageProducts.vue', '{
-- "guest": true
-- }', 1);
-- INSERT INTO `b2b_routes` (`id`, `name`, `path`, `component`, `meta`, `active`) VALUES (10, 'customer-addresses', 'addresses', '/components/customer/PageAddresses.vue', '{"guest":true}', 1);
-- INSERT INTO `b2b_routes` (`id`, `name`, `path`, `component`, `meta`, `active`) VALUES (11, 'customer-carts', 'carts', '/components/customer/PageCarts.vue', '{"guest":true}', 1);
-- INSERT INTO `b2b_routes` (`id`, `name`, `path`, `component`, `meta`, `active`) VALUES (12, 'customer-orders', 'orders', '/components/customer/PageOrders.vue', '{"guest":true}', 1);
-- INSERT INTO `b2b_routes` (`id`, `name`, `path`, `component`, `meta`, `active`) VALUES (13, 'customer-statistic', 'statistic', '/components/customer/PageStatistic.vue', '{"guest":true}', 1);
-- INSERT INTO `b2b_routes` (`id`, `name`, `path`, `component`, `meta`, `active`) VALUES (14, 'customer-product-details', 'products/:product_id', '/components/admin/ProductDetailView.vue', '{"guest":true}', 1);
-- INSERT INTO `b2b_routes` (`id`, `name`, `path`, `component`, `meta`, `active`) VALUES (15, 'admin-products', 'products', '/components/admin/PageProducts.vue', '{"guest":true}', 1);
-- INSERT IGNORE INTO b2b_routes
-- (name, path, component, meta, active)
-- VALUES
-- ('root', '', '', '{"trans": "route.root"}', 0),
-- ('home', '', '/views/HomeView.vue', '{"trans": "route.home"}', 1),
-- ('login', 'login', '/views/LoginView.vue', '{"guest":true}', 1),
-- ('register', 'register', '/views/RegisterView.vue', '{"guest":true}', 1),
-- ('password-recovery', 'password-recovery', '/views/PasswordRecoveryView.vue', '{"guest":true}', 1),
-- ('reset-password', 'reset-password', '/views/ResetPasswordView.vue', '{"guest":true}', 1),
-- ('verify-email', 'verify-email', '/views/VerifyEmailView.vue', '{"guest":true}', 1);
INSERT IGNORE INTO `b2b_routes` (`id`, `name`, `path`, `component`, `meta`, `active`) VALUES
(1, 'root', '', '', '{"trans": "route.root"}', 0),
(2, 'home', '', '/views/HomeView.vue', '{"trans": "route.home"}', 1),
(3, 'login', 'login', '/views/LoginView.vue', '{"guest":true}', 1),
(4, 'register', 'register', '/views/RegisterView.vue', '{"guest":true}', 1),
(5, 'password-recovery', 'password-recovery', '/views/PasswordRecoveryView.vue', '{"guest":true}', 1),
(6, 'reset-password', 'reset-password', '/views/ResetPasswordForm.vue', '{"guest":true}', 1),
(7, 'verify-email', 'verify-email', '/views/VerifyEmailView.vue', '{"guest":true}', 1),
(8, 'category', 'category/:category_id-:link_rewrite', '/views/CategoryView.vue', '{"guest":true}', 1),
(9, 'admin-products-category', 'products/:category_id-:link_rewrite', '/components/admin/PageProducts.vue', '{
"guest": true
}', 1),
(10, 'customer-addresses', 'addresses', '/components/customer/PageAddresses.vue', '{"guest":true}', 1),
(11, 'customer-carts', 'carts', '/components/customer/PageCarts.vue', '{"guest":true}', 1),
(12, 'customer-orders', 'orders', '/components/customer/PageOrders.vue', '{"guest":true}', 1),
(13, 'customer-statistic', 'statistic', '/components/customer/PageStatistic.vue', '{"guest":true}', 1),
(14, 'customer-product-details', 'products/:product_id/:link_rewrite', '/components/admin/ProductDetailView.vue', '{"guest":true}', 1),
(15, 'admin-products', 'products', '/components/admin/PageProducts.vue', '{"guest":true}', 1);
CREATE TABLE IF NOT EXISTS b2b_top_menu (
menu_id INT AUTO_INCREMENT NOT NULL,
@@ -54,97 +54,91 @@ CREATE TABLE IF NOT EXISTS b2b_top_menu (
INDEX FK_b2b_top_menu_parent_id_idx (parent_id ASC)
) ENGINE = InnoDB;
INSERT IGNORE INTO `b2b_top_menu` (`menu_id`, `label`, `parent_id`, `params`, `active`, `position`) VALUES
(1, JSON_COMPACT('{"name":"root","trans":{"pl":{"label":"Menu główne"},"en":{"label":"Main Menu"},"de":{"label":"Hauptmenü"}}}'),NULL,JSON_COMPACT('{}'),1,1),
(3, JSON_COMPACT('{"name":"admin-products","trans":{"pl":{"label":"admin-products"},"en":{"label":"admin-products"},"de":{"label":"admin-products"}}}'),1,JSON_COMPACT('{}'),1,1),
(9, JSON_COMPACT('{"name":"carts","trans":{"pl":{"label":"Koszyki"},"en":{"label":"Carts"},"de":{"label":"Warenkörbe"}}}'),3,JSON_COMPACT('{"route": {"name": "home", "params":{"locale": ""}}}'),1,1);
-- INSERT INTO `b2b_top_menu` (`menu_id`, `label`, `parent_id`, `params`, `active`, `position`) VALUES (1, '{"name":"root","trans":{"pl":{"label":"Menu główne"},"en":{"label":"Main Menu"},"de":{"label":"Hauptmenü"}}}', NULL, '{}', 1, 1);
-- INSERT INTO `b2b_top_menu` (`menu_id`, `label`, `parent_id`, `params`, `active`, `position`) VALUES (2, '{
-- "name": "admin-products",
-- "trans": {
-- "pl": {
-- "label": "Products"
-- },
-- "en": {
-- "label": "Products"
-- },
-- "de": {
-- "label": "Products"
-- }
-- },
-- "icon" : "quill:list"
-- }', 1, '{"route":{"name":"admin-products","params":{"locale":""}}}', 1, 1);
-- INSERT INTO `b2b_top_menu` (`menu_id`, `label`, `parent_id`, `params`, `active`, `position`) VALUES (3, '{
-- "name": "customer-carts",
-- "trans": {
-- "pl": {
-- "label": "Carts"
-- },
-- "en": {
-- "label": "Carts"
-- },
-- "de": {
-- "label": "Carts"
-- }
-- },
-- "icon" : "proicons:cart"
-- }', 1, '{"route":{"name":"customer-carts","params":{"locale":""}}}', 1, 1);
-- INSERT INTO `b2b_top_menu` (`menu_id`, `label`, `parent_id`, `params`, `active`, `position`) VALUES (4, '{
-- "name": "customer-addresses",
-- "trans": {
-- "pl": {
-- "label": "Addresses"
-- },
-- "en": {
-- "label": "Addresses"
-- },
-- "de": {
-- "label": "Addresses"
-- }
-- },
-- "icon" : "ph:address-book"
-- }', 1, '{"route":{"name":"customer-addresses","params":{"locale":""}}}', 1, 1);
-- INSERT INTO `b2b_top_menu` (`menu_id`, `label`, `parent_id`, `params`, `active`, `position`) VALUES (5, '{
-- "name": "customer-orders",
-- "trans": {
-- "pl": {
-- "label": "Orders"
-- },
-- "en": {
-- "label": "Orders"
-- },
-- "de": {
-- "label": "Orders"
-- }
-- },
-- "icon" : "lsicon:order-integral-outline"
-- }', 1, '{"route":{"name":"customer-orders","params":{"locale":""}}}', 1, 1);
-- INSERT INTO `b2b_top_menu` (`menu_id`, `label`, `parent_id`, `params`, `active`, `position`) VALUES (6, '{
-- "name": "customer-statistic",
-- "trans": {
-- "pl": {
-- "label": "Statistic"
-- },
-- "en": {
-- "label": "Statistic"
-- },
-- "de": {
-- "label": "Statistic"
-- }
-- },
-- "icon" : "akar-icons:statistic-up"
-- }', 1, '{
-- "route": {
-- "name": "customer-statistic",
-- "params": {
-- "locale": ""
-- }
-- }
-- }', 1, 1);
-- INSERT INTO `b2b_top_menu` (`menu_id`, `label`, `parent_id`, `params`, `active`, `position`) VALUES (9, '{"name":"carts","trans":{"pl":{"label":"Koszyki"},"en":{"label":"Carts"},"de":{"label":"Warenkörbe"}}}', 3, '{"route":{"name":"home","params":{"locale":""}}}', 1, 1);
(1, '{"name":"root","trans":{"pl":{"label":"Menu główne"},"en":{"label":"Main Menu"},"de":{"label":"Hauptmenü"}}}', NULL, '{}', 1, 1),
(2, '{
"name": "admin-products",
"trans": {
"pl": {
"label": "Products"
},
"en": {
"label": "Products"
},
"de": {
"label": "Products"
}
},
"icon" : "quill:list"
}', 1, '{"route":{"name":"admin-products","params":{"locale":""}}}', 1, 1),
(3, '{
"name": "customer-carts",
"trans": {
"pl": {
"label": "Carts"
},
"en": {
"label": "Carts"
},
"de": {
"label": "Carts"
}
},
"icon" : "proicons:cart"
}', 1, '{"route":{"name":"customer-carts","params":{"locale":""}}}', 1, 1),
(4, '{
"name": "customer-addresses",
"trans": {
"pl": {
"label": "Addresses"
},
"en": {
"label": "Addresses"
},
"de": {
"label": "Addresses"
}
},
"icon" : "ph:address-book"
}', 1, '{"route":{"name":"customer-addresses","params":{"locale":""}}}', 1, 1),
(5, '{
"name": "customer-orders",
"trans": {
"pl": {
"label": "Orders"
},
"en": {
"label": "Orders"
},
"de": {
"label": "Orders"
}
},
"icon" : "lsicon:order-integral-outline"
}', 1, '{"route":{"name":"customer-orders","params":{"locale":""}}}', 1, 1),
(6, '{
"name": "customer-statistic",
"trans": {
"pl": {
"label": "Statistic"
},
"en": {
"label": "Statistic"
},
"de": {
"label": "Statistic"
}
},
"icon" : "akar-icons:statistic-up"
}', 1, '{
"route": {
"name": "customer-statistic",
"params": {
"locale": ""
}
}
}', 1, 1),
(9, '{"name":"carts","trans":{"pl":{"label":"Koszyki"},"en":{"label":"Carts"},"de":{"label":"Warenkörbe"}}}', 3, '{"route":{"name":"home","params":{"locale":""}}}', 1, 1);

View File

@@ -11,7 +11,6 @@ INSERT INTO `b2b_roles` (`name`, `id`) VALUES ('user','1');
INSERT INTO `b2b_roles` (`name`, `id`) VALUES ('admin','2');
INSERT INTO `b2b_roles` (`name`, `id`) VALUES ('super_admin','3');
-- insert sample admin user admin@ma-al.com/Maal12345678
INSERT IGNORE INTO b2b_customers (id, email, password, first_name, last_name, role_id, provider, provider_id, avatar_url, is_active, email_verified, email_verification_token, email_verification_expires, password_reset_token, password_reset_expires, last_password_reset_request, last_login_at, lang_id, country_id, created_at, updated_at, deleted_at)
VALUES
@@ -30,6 +29,4 @@ VALUES
(3, '🇨🇿', 16, 2),
(4, '🇩🇪', 1, 2);
-- +goose Down