5 Commits

15 changed files with 758 additions and 386 deletions

1
bo/components.d.ts vendored
View File

@@ -14,6 +14,7 @@ declare module 'vue' {
CartDetails: typeof import('./src/components/customer/CartDetails.vue')['default'] CartDetails: typeof import('./src/components/customer/CartDetails.vue')['default']
CategoryMenu: typeof import('./src/components/inner/CategoryMenu.vue')['default'] CategoryMenu: typeof import('./src/components/inner/CategoryMenu.vue')['default']
CategoryMenuListing: typeof import('./src/components/inner/categoryMenuListing.vue')['default'] CategoryMenuListing: typeof import('./src/components/inner/categoryMenuListing.vue')['default']
copy: typeof import('./src/components/admin/ProductDetailView copy.vue')['default']
CountryCurrencySwitch: typeof import('./src/components/inner/CountryCurrencySwitch.vue')['default'] CountryCurrencySwitch: typeof import('./src/components/inner/CountryCurrencySwitch.vue')['default']
Cs_PrivacyPolicyView: typeof import('./src/components/terms/cs_PrivacyPolicyView.vue')['default'] Cs_PrivacyPolicyView: typeof import('./src/components/terms/cs_PrivacyPolicyView.vue')['default']
Cs_TermsAndConditionsView: typeof import('./src/components/terms/cs_TermsAndConditionsView.vue')['default'] Cs_TermsAndConditionsView: typeof import('./src/components/terms/cs_TermsAndConditionsView.vue')['default']

View File

@@ -7,3 +7,37 @@ import { RouterView } from 'vue-router'
<RouterView /> <RouterView />
</Suspense> </Suspense>
</template> </template>
<!-- <template>
<component :is="layoutComponent">
<Suspense>
<RouterView />
</Suspense>
</component>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { useRoute } from 'vue-router'
import DefaultLayout from '@/layouts/default.vue'
import EmptyLayout from '@/layouts/empty.vue'
const route = useRoute()
const layouts = {
default: DefaultLayout,
auth: EmptyLayout
}
console.log(route.fullPath)
console.log(route.name)
console.log(route.matched)
const layoutComponent = computed(() => {
console.log(route.meta);
return layouts[route.meta.layout as keyof typeof layouts] || DefaultLayout
})
</script> -->

View File

@@ -8,9 +8,9 @@ export const uiOptions: NuxtUIOptions = {
} }
}, },
button: { button: {
slots: { // slots: {
base: 'border! border-(--border-light)! dark:border-(--border-dark)! outline-0! ring-0!', // base: 'border! border-(--border-light)! dark:border-(--border-dark)! outline-0! ring-0!',
}, // },
}, },
input: { input: {
slots: { slots: {
@@ -40,9 +40,10 @@ export const uiOptions: NuxtUIOptions = {
}, },
selectMenu: { selectMenu: {
slots: { slots: {
base: 'border! border-(--border-light)! dark:border-(--border-dark)! outline-0! ring-0!', // base: 'border! border-(--border-light)! dark:border-(--border-dark)! outline-0! ring-0!',
content: 'border! border-(--border-light)! dark:border-(--border-dark)! outline-0! ring-0! z-80 text-(--black)! dark:text-white!', // content: 'border! border-(--border-light)! dark:border-(--border-dark)! outline-0! ring-0! z-80',
itemLeadingIcon: 'text-(--black)! dark:text-white!' // content: 'border! border-(--border-light)! dark:border-(--border-dark)! outline-0! ring-0! z-80 text-(--black)! dark:text-white!',
// itemLeadingIcon: 'text-(--black)! dark:text-white!'
} }
}, },

1
bo/src/assets/error.svg Normal file
View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><g fill="none"><path stroke="#5b5b5b" stroke-width="2" d="M3 11c0-3.771 0-5.657 1.172-6.828S7.229 3 11 3h2c3.771 0 5.657 0 6.828 1.172S21 7.229 21 11v2c0 3.771 0 5.657-1.172 6.828S16.771 21 13 21h-2c-3.771 0-5.657 0-6.828-1.172S3 16.771 3 13z"/><path fill="#5b5b5b" fill-rule="evenodd" d="m19 13.585l-.02-.02c-.39-.39-.726-.726-1.026-.979c-.317-.267-.662-.502-1.088-.63a3 3 0 0 0-1.732 0c-.426.129-.77.363-1.088.63c-.3.253-.636.59-1.025.98l-.029.028c-.307.306-.487.486-.628.601l-.02.017l-.012-.023c-.087-.16-.189-.393-.36-.792l-.053-.124l-.022-.052c-.356-.83-.655-1.528-.95-2.054c-.305-.541-.685-1.05-1.277-1.346a3 3 0 0 0-1.597-.307c-.66.055-1.201.386-1.685.775c-.406.327-.86.77-1.388 1.297V13q0 .774.003 1.411l1.114-1.114c.69-.69 1.15-1.147 1.525-1.45c.37-.298.53-.335.6-.34a1 1 0 0 1 .532.102c.061.031.196.124.43.54c.236.42.493 1.016.877 1.912l.053.124l.017.038c.149.348.287.67.425.924c.145.265.355.583.709.802a2 2 0 0 0 1.398.27c.41-.073.723-.29.956-.482c.222-.184.47-.432.738-.7l.03-.03c.425-.425.701-.7.928-.891c.218-.184.32-.228.376-.245a1 1 0 0 1 .578 0c.056.017.158.061.376.245c.227.191.503.466.929.892l1.35 1.35c.046-.718.054-1.61.056-2.773" clip-rule="evenodd"/><circle cx="16.5" cy="7.5" r="1.5" fill="#5b5b5b"/></g></svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -9,7 +9,7 @@ body {
font-family: "Inter", sans-serif; font-family: "Inter", sans-serif;
} }
.container{ .container {
max-width: 2100px; max-width: 2100px;
} }
@@ -30,7 +30,7 @@ body {
/* text */ /* text */
--accent-blue-dark: #3B82F6; --accent-blue-dark: #3B82F6;
--accent-blue-light:#2563EB; --accent-blue-light: #2563EB;
--text-dark: #FFFEFB; --text-dark: #FFFEFB;
/* placeholder */ /* placeholder */
@@ -52,22 +52,25 @@ body {
--ui-bg-elevated: var(--color-gray-500); --ui-bg-elevated: var(--color-gray-500);
--ui-error: var(--dark-red); --ui-error: var(--dark-red);
--border: var(--border-dark); --border: var(--border-dark);
--tw-border-style: var(--border-dark); --tw-border-style: var(--border-dark);
} }
.label-form { .label-form {
@apply text-(--gray) dark:text-(--gray-dark) pl-0 md:pl-6 leading-none; @apply text-(--gray) dark:text-(--gray-dark) pl-0 md:pl-6 leading-none;
} }
.title { .title {
@apply font-medium text-[19px] sm:text-xl md:text-[22px] leading-none text-(--black) dark:text-(--main-light); @apply font-medium text-[19px] sm:text-xl md:text-[22px] leading-none text-(--black) dark:text-(--main-light);
} }
.column-title { .column-title {
@apply md:ml-[25px] mb-[25px] sm:mb-[30px]; @apply md:ml-[25px] mb-[25px] sm:mb-[30px];
} }
.form-title { .form-title {
@apply text-(--accent-green) dark:text-(--accent-green-dark) font-medium; @apply text-(--accent-green) dark:text-(--accent-green-dark) font-medium;
} }
.blue-button {
@apply bg-info! text-white!
}

View File

@@ -1,32 +1,32 @@
<template> <template>
<header <header
class="fixed top-0 left-0 right-0 z-50 bg-white/80 dark:bg-(--black) backdrop-blur-md border-b border-(--border-light) dark:border-(--border-dark)"> class="fixed top-0 left-0 right-0 z-50 bg-white/80 dark:bg-(--black) backdrop-blur-md border-b border-(--border-light) dark:border-(--border-dark)">
<!-- px-4 sm:px-6 lg:px-8 -->
<div class="container mx-auto px-4"> <div class="container mx-auto px-4">
<div class="flex items-center justify-between h-14"> <div class="flex items-center justify-between h-16">
<!-- Logo -->
<RouterLink :to="{ name: 'home' }" class="flex items-center gap-2"> <RouterLink :to="{ name: 'home' }" class="flex items-center gap-2">
<div class="w-8 h-8 rounded-lg bg-primary text-white flex items-center justify-center"> <div class="w-8 h-8 rounded-lg bg-primary text-white flex items-center justify-center">
<UIcon name="i-heroicons-clock" class="w-5 h-5" /> <UIcon name="i-heroicons-clock" class="w-5 h-5" />
</div> </div>
<span class="font-semibold text-gray-900 dark:text-white">TimeTracker</span> <span class="font-semibold text-gray-900 dark:text-white">{{ settings.app.name }}</span>
</RouterLink> </RouterLink>
<UNavigationMenu :type="'trigger'" :ui="{ <UNavigationMenu :type="'trigger'" :ui="{
root: 'justify-center', root: 'justify-center',
list: 'gap-4' list: 'gap-4 text-(--black)',
}" :items="menuItems" class="w-full"></UNavigationMenu> linkLabel: 'text-(--black) text-sm!'
<div class="flex items-center gap-2"> }" :items="menuItems" class="" />
<CountryCurrencySwitch /> <div class="flex items-center gap-12">
<!-- Language Switcher --> <div class="flex items-center gap-2">
<LangSwitch /> <CountryCurrencySwitch />
<!-- Theme Switcher --> <LangSwitch />
<ThemeSwitch /> </div>
<!-- Logout Button (only when authenticated) --> <div class="flex items-center gap-2">
<button v-if="authStore.isAuthenticated" @click="authStore.logout()" <ThemeSwitch />
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)"> <button v-if="authStore.isAuthenticated" @click="authStore.logout()"
{{ $t('general.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">
</button> {{ $t('general.logout') }}
</button>
</div>
</div> </div>
</div> </div>
</div> </div>
@@ -44,6 +44,7 @@ import type { LabelTrans, TopMenuItem } from '@/types'
import type { NavigationMenuItem } from '@nuxt/ui' import type { NavigationMenuItem } from '@nuxt/ui'
import { useRoute, useRouter } from 'vue-router' import { useRoute, useRouter } from 'vue-router'
import CountryCurrencySwitch from './inner/CountryCurrencySwitch.vue' import CountryCurrencySwitch from './inner/CountryCurrencySwitch.vue'
import { settings } from '@/router/settings'
const authStore = useAuthStore() const authStore = useAuthStore()
let menu = ref() let menu = ref()

View File

@@ -7,7 +7,7 @@
<UTable :data="productsList" :columns="columns" class="flex-1 w-full" /> <UTable :data="productsList" :columns="columns" class="flex-1 w-full" />
<UPagination v-model:page="page" :total="total" :items-per-page="perPage" /> <UPagination v-model:page="page" :total="total" :items-per-page="perPage" />
</div> </div>
</div> --> </div>
</component> </component>
</suspense> </suspense>
</template> </template>
@@ -173,6 +173,7 @@ const UInput = resolveComponent('UInput')
const UButton = resolveComponent('UButton') const UButton = resolveComponent('UButton')
const UIcon = resolveComponent('UIcon') const UIcon = resolveComponent('UIcon')
import errorImg from '@/assets/error.svg'
const columns: TableColumn<Product>[] = [ const columns: TableColumn<Product>[] = [
{ {
accessorKey: 'product_id', accessorKey: 'product_id',
@@ -209,9 +210,13 @@ const columns: TableColumn<Product>[] = [
cell: ({ row }) => { cell: ({ row }) => {
return h('img', { return h('img', {
src: row.getValue('image_link') as string, src: row.getValue('image_link') as string,
style: 'width:40px;height:40px;object-fit:cover;' style: 'width:40px;height:40px;object-fit:cover;',
onError: (e: Event) => {
const target = e.target as HTMLImageElement
target.src = errorImg
}
}) })
} },
}, },
{ {
accessorKey: 'name', accessorKey: 'name',
@@ -272,8 +277,9 @@ const columns: TableColumn<Product>[] = [
onClick: () => { onClick: () => {
goToProduct(row.original.product_id) goToProduct(row.original.product_id)
}, },
color: 'primary', class: 'cursor-pointer',
variant: 'solid' color: 'info',
variant: 'soft'
}, () => 'Show product') }, () => 'Show product')
}, },
} }

View File

@@ -1,61 +1,32 @@
<template> <template>
<component :is="Default || 'div'"> <component :is="Default || 'div'">
<div class="container my-10 mx-auto">
<div class="flex items-end justify-between gap-4 mb-6 bg-(--second-light) dark:bg-(--main-dark) border border-(--border-light) dark:border-(--border-dark) p-4 rounded-md"> <div class="container my-10 mx-auto ">
<div
<div class="flex items-center gap-3"> class="flex items-end justify-between gap-4 mb-6 bg-(--second-light) dark:bg-(--main-dark) border border-(--border-light) dark:border-(--border-dark) p-4 rounded-md">
<div class="flex items-end gap-3">
<template v-if="!isViewingExistingTranslation"> <USelect v-model="selectedLanguage" :items="availableLangs" variant="outline" class="w-40!" valueKey="id">
<USelectMenu <template #default="{ modelValue }">
v-model="selectedLangId" <div class="flex items-center gap-2">
:items="langOptions" <span class="text-md">{{availableLangs.find(x => x.id == modelValue)?.flag}}</span>
value-key="id" <span class="font-medium dark:text-white text-black">{{availableLangs.find(x => x.id ==
class="w-40" modelValue)?.name}}</span>
:search-input="false" </div>
> </template>
<template #default="{ modelValue }"> <template #item-leading="{ item }">
<span class="dark:text-white text-black"> <div class="flex items-center rounded-md cursor-pointer transition-colors">
{{ langOptions.find(l => l.id === modelValue)?.name }} <span class="text-md">{{ item.flag }}</span>
</span> <span class="ml-2 dark:text-white text-black font-medium">{{ item.name }}</span>
</template> </div>
</template>
<template #item-leading="{ item }"> </USelect>
<span class="dark:text-white text-black font-medium">
{{ item.name }}
</span>
</template>
</USelectMenu>
<UButton
@click="translateOrSave"
:disabled="selectedLangId === defaultLangId"
:loading="translating"
class="text-white bg-(--accent-blue-light) dark:bg-(--accent-blue-dark) px-12!"
>
Translate
</UButton>
</template>
<template v-else>
<UButton
@click="goBackToDefault"
color="neutral"
variant="outline"
class="cursor-pointer"
>
<UIcon name="i-lucide-arrow-left" class="mr-1" />
<p class="dark:text-white">Back to Polish</p>
</UButton>
</template>
</div> </div>
<UButton @click="translateToSelectedLanguage" color="primary" :loading="translating"
class="text-white bg-(--accent-blue-light) dark:bg-(--accent-blue-dark) px-12!">
Translate
</UButton>
</div> </div>
<p v-if="noTranslation" class="text-red-500 mt-2">
No translation available
</p>
<div v-if="translating" class="fixed inset-0 z-50 flex items-center justify-center bg-black/50"> <div v-if="translating" class="fixed inset-0 z-50 flex items-center justify-center bg-black/50">
<div class="flex flex-col items-center gap-4 p-8 bg-(--main-light) dark:bg-(--main-dark) rounded-lg shadow-xl"> <div class="flex flex-col items-center gap-4 p-8 bg-(--main-light) dark:bg-(--main-dark) rounded-lg shadow-xl">
<UIcon name="svg-spinners:ring-resize" class="text-4xl text-primary" /> <UIcon name="svg-spinners:ring-resize" class="text-4xl text-primary" />
@@ -66,7 +37,6 @@
<div v-if="productStore.loading" class="flex items-center justify-center py-20"> <div v-if="productStore.loading" class="flex items-center justify-center py-20">
<UIcon name="svg-spinners:ring-resize" class="text-4xl text-primary" /> <UIcon name="svg-spinners:ring-resize" class="text-4xl text-primary" />
</div> </div>
<div v-else-if="productStore.error" class="flex items-center justify-center py-20"> <div v-else-if="productStore.error" class="flex items-center justify-center py-20">
<p class="text-red-500">{{ productStore.error }}</p> <p class="text-red-500">{{ productStore.error }}</p>
</div> </div>
@@ -75,14 +45,11 @@
<div class="w-80 h-80 bg-(--second-light) dark:bg-gray-700 rounded-lg flex items-center justify-center"> <div class="w-80 h-80 bg-(--second-light) dark:bg-gray-700 rounded-lg flex items-center justify-center">
<span class="text-gray-500 dark:text-gray-400">Product Image</span> <span class="text-gray-500 dark:text-gray-400">Product Image</span>
</div> </div>
<div class="flex flex-col gap-2"> <div class="flex flex-col gap-2">
<p class="text-[25px] font-bold text-black dark:text-white"> <p class="text-[25px] font-bold text-black dark:text-white">
{{ productName || 'Product Name' }} {{ productStore.productDescription.name || 'Product Name' }}
</p> </p>
<p v-html="productStore.productDescription.description_short" class="text-black dark:text-white"></p>
<p v-html="displayedShortDescription" class="text-black dark:text-white"></p>
<div class="space-y-[10px]"> <div class="space-y-[10px]">
<div class="flex items-center gap-1"> <div class="flex items-center gap-1">
<UIcon name="lets-icons:done-ring-round-fill" class="text-[20px] text-green-600" /> <UIcon name="lets-icons:done-ring-round-fill" class="text-[20px] text-green-600" />
@@ -90,7 +57,6 @@
{{ productStore.productDescription.available_now }} {{ productStore.productDescription.available_now }}
</p> </p>
</div> </div>
<div class="flex items-center gap-1"> <div class="flex items-center gap-1">
<UIcon name="marketeq:car-shipping" class="text-[25px] text-green-600" /> <UIcon name="marketeq:car-shipping" class="text-[25px] text-green-600" />
<p class="text-[18px] font-bold text-black dark:text-white"> <p class="text-[18px] font-bold text-black dark:text-white">
@@ -101,43 +67,327 @@
</div> </div>
</div> </div>
<!-- TABS -->
<div v-if="productStore.productDescription" class="mt-16"> <div v-if="productStore.productDescription" class="mt-16">
<div class="flex gap-4 my-6"> <div class="flex gap-4 my-6">
<UButton @click="activeTab = 'description'" :class="btnClass('description')" variant="outline"> <UButton @click="activeTab = 'description'"
Description :class="['cursor-pointer', activeTab === 'description' ? 'bg-blue-500 text-white' : '']" color="neutral"
variant="outline">
<p class="dark:text-white">Description</p>
</UButton> </UButton>
<UButton @click="activeTab = 'usage'" :class="btnClass('usage')" variant="outline"> <UButton @click="activeTab = 'usage'"
Usage :class="['cursor-pointer', activeTab === 'usage' ? 'bg-blue-500 text-white' : '']" color="neutral"
variant="outline">
<p class="dark:text-white">Usage</p>
</UButton> </UButton>
</div> </div>
<!-- USAGE --> <div v-if="activeTab === 'usage'"
<div v-if="activeTab === 'usage'" class="box"> class="px-8 py-4 border border-(--border-light) dark:border-(--border-dark) rounded-md bg-(--second-light) dark:bg-(--main-dark)">
<div class="flex justify-end mb-4 gap-2">
<UButton v-if="!isEditing" @click="enableUsageEdit" class="bg-(--accent-blue-light)! dark:bg-(--accent-blue-dark)! text-white px-3 py-1 rounded"> <div class="flex justify-end items-center gap-3 mb-4">
Edit Usage <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-white">Change Text</p>
<UIcon name="material-symbols-light:stylus-note-sharp" class="text-[30px] text-white!" />
</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> </UButton>
<UButton v-if="isEditing" @click="saveUsage" class="px-3 py-1 border rounded">Save</UButton>
<UButton v-if="isEditing" @click="cancelUsageEdit" class="px-3 py-1 border rounded">Cancel</UButton>
</div> </div>
<p ref="usageRef" v-html="displayedUsage || 'Usage information not available'" class="text-black dark:text-white"></p> <p ref="usageRef" v-html="productStore.productDescription.usage"
class="flex flex-col justify-center w-full text-start dark:text-white! text-black!"></p>
</div> </div>
<!-- DESCRIPTION --> <div v-if="activeTab === 'description'"
<div v-if="activeTab === 'description'" class="box"> class="px-8 py-4 border border-(--border-light) dark:border-(--border-dark) rounded-md bg-(--second-light) dark:bg-(--main-dark)">
<div class="flex justify-end mb-4 gap-2"> <div class="flex items-center justify-end gap-3 mb-4">
<UButton v-if="!descriptionEdit.isEditing.value" @click="enableDescriptionEdit" class="bg-(--accent-blue-light)! dark:bg-(--accent-blue-dark)! text-white px-3 py-1 rounded"> <UButton v-if="!descriptionEdit.isEditing.value" @click="enableDescriptionEdit"
Edit Description 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>
<UButton v-if="descriptionEdit.isEditing.value" @click="saveDescription" class="px-3 py-1 border rounded">Save</UButton> <UButton v-if="descriptionEdit.isEditing.value" @click="saveDescription" color="neutral" variant="outline"
<UButton v-if="descriptionEdit.isEditing.value" @click="cancelDescriptionEdit" class="px-3 py-1 border rounded">Cancel</UButton> class="p-2.5 cursor-pointer">
<p class="dark:text-white text-black ">Save the edited text</p>
</UButton>
<UButton v-if="descriptionEdit.isEditing.value" @click="cancelDescriptionEdit" color="neutral"
variant="outline" class="p-2.5 cursor-pointer">Cancel</UButton>
</div>
<div ref="descriptionRef" v-html="productStore.productDescription.description"
class="flex flex-col justify-center dark:text-white text-black">
</div> </div>
<div ref="descriptionRef" v-html="displayedDescription || 'Description not available'" class="text-black dark:text-white"></div>
</div> </div>
</div> </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 { Language } from '@/types';
import { watch } from 'vue';
import { onMounted, ref } from 'vue';
import { computed } from 'vue';
import { useRoute } from 'vue-router';
const route = useRoute()
const settingStore = useSettingsStore()
const productStore = useProductStore()
const selectedLanguage = ref(settingStore.shopDefaultLanguage)
const availableLangs = computed(() => langs)
const productID = ref<number>(0)
const toLangId = ref(settingStore.shopDefaultLanguage)
const defaultLangId = ref(settingStore.shopDefaultLanguage)
const translating = ref(false)
const activeTab = ref('description')
const usageRef = ref<HTMLElement | null>(null)
const originalUsage = ref('')
const isEditing = ref(false)
const usageEdit = useEditable(usageRef)
const descriptionRef = ref<HTMLElement | null>(null)
const descriptionEdit = useEditable(descriptionRef)
const originalDescription = ref('')
// Коли користувач обирає мову, ця функція бере текст продукту з основної мови і перекладає його на вибрану мову. Поки переклад йде показує спінер. Коли переклад готовий, активна мова оновлюється, а спінер зникає.
const translateToSelectedLanguage = async () => {
const targetLang = langs.find((l: Language) => l.id === selectedLanguage.value)
if (targetLang && toLangId.value && productID.value) {
translating.value = true
try {
await productStore.translateProductDescription(productID.value, toLangId.value, defaultLangId.value)
toLangId.value = targetLang.id
} finally {
translating.value = false
}
}
}
// знайти мову → підвантажити опис → запам’ятати, що це “базова” мова для перекладу.
const fetchForLanguage = async (langCode: number) => {
const lang = langs.find((l: Language) => l.id === langCode)
if (lang && productID.value) {
await productStore.getProductDescription(lang.id, productID.value)
toLangId.value = lang.id
}
}
watch(selectedLanguage, async (newLang: number) => {
if (productID.value) {
await fetchForLanguage(newLang)
}
})
onMounted(async () => {
const id = route.params.product_id
if (id) {
productID.value = Number(id)
await fetchForLanguage(selectedLanguage.value)
}
})
const enableEdit = () => {
if (usageRef.value) {
originalUsage.value = usageRef.value.innerHTML
}
isEditing.value = true
usageEdit.enableEdit()
}
const saveText = () => {
if (usageRef.value) {
productStore.productDescription.usage = usageRef.value.innerHTML
}
usageEdit.disableEdit()
isEditing.value = false
productStore.saveProductDescription(productID.value, toLangId.value)
}
const cancelEdit = () => {
if (usageRef.value) {
usageRef.value.innerHTML = originalUsage.value
}
usageEdit.disableEdit()
isEditing.value = false
}
const enableDescriptionEdit = () => {
if (descriptionRef.value) {
originalDescription.value = descriptionRef.value.innerHTML
}
descriptionEdit.enableEdit()
}
const saveDescription = async () => {
if (descriptionRef.value) {
productStore.productDescription.description = descriptionRef.value.innerHTML
}
descriptionEdit.disableEdit()
await productStore.saveProductDescription(productID.value, toLangId.value)
}
const cancelDescriptionEdit = () => {
if (descriptionRef.value) {
descriptionRef.value.innerHTML = originalDescription.value
}
descriptionEdit.disableEdit()
}
</script>
<!-- <template>
<component :is="Default || 'div'">
<div class="container my-10 mx-auto ">
<div
class="flex items-end justify-between gap-4 mb-6 bg-(--second-light) dark:bg-(--main-dark) border border-(--border-light) dark:border-(--border-dark) p-4 rounded-md">
<div class="flex items-end gap-3">
<USelect v-model="selectedLanguage" :items="availableLangs" variant="outline" class="w-40!" valueKey="id">
<template #default="{ modelValue }">
<div class="flex items-center gap-2">
<span class="text-md">{{availableLangs.find(x => x.id == modelValue)?.flag}}</span>
<span class="font-medium dark:text-white text-black">{{availableLangs.find(x => x.id ==
modelValue)?.name}}</span>
</div>
</template>
<template #item-leading="{ item }">
<div class="flex items-center rounded-md cursor-pointer transition-colors">
<span class="text-md">{{ item.flag }}</span>
<span class="ml-2 dark:text-white text-black font-medium">{{ item.name }}</span>
</div>
</template>
</USelect>
</div>
<UButton @click="translateToSelectedLanguage" color="primary" :loading="translating"
class="text-white bg-(--accent-blue-light) dark:bg-(--accent-blue-dark) px-12!">
Translate
</UButton>
</div>
<div v-if="translating" class="fixed inset-0 z-50 flex items-center justify-center bg-black/50">
<div class="flex flex-col items-center gap-4 p-8 bg-(--main-light) dark:bg-(--main-dark) rounded-lg shadow-xl">
<UIcon name="svg-spinners:ring-resize" class="text-4xl text-primary" />
<p class="text-lg font-medium dark:text-white text-black">Translating...</p>
</div>
</div>
<div v-if="productStore.loading" class="flex items-center justify-center py-20">
<UIcon name="svg-spinners:ring-resize" class="text-4xl text-primary" />
</div>
<div v-else-if="productStore.error" class="flex items-center justify-center py-20">
<p class="text-red-500">{{ productStore.error }}</p>
</div>
<div v-else-if="productStore.productDescription" class="flex items-start gap-30">
<div class="w-80 h-80 bg-(--second-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 gap-2">
<p class="text-[25px] font-bold text-black dark:text-white">
{{ productStore.productDescription.name || 'Product Name' }}
</p>
<p v-html="productStore.productDescription.description_short" class="text-black dark:text-white"></p>
<div class="space-y-[10px]">
<div class="flex items-center gap-1">
<UIcon name="lets-icons:done-ring-round-fill" class="text-[20px] text-green-600" />
<p class="text-[16px] font-bold text-(--accent-blue-light) dark:text-(--accent-blue-dark)">
{{ productStore.productDescription.available_now }}
</p>
</div>
<div class="flex items-center gap-1">
<UIcon name="marketeq:car-shipping" class="text-[25px] text-green-600" />
<p class="text-[18px] font-bold text-black dark:text-white">
{{ productStore.productDescription.delivery_in_stock || 'Delivery information' }}
</p>
</div>
</div>
</div>
</div>
<div v-if="productStore.productDescription" class="mt-16">
<div class="flex gap-4 my-6">
<UButton @click="activeTab = 'description'"
:class="['cursor-pointer', activeTab === 'description' ? 'bg-blue-500 text-white' : '']" color="neutral"
variant="outline">
<p class="dark:text-white">Description</p>
</UButton>
<UButton @click="activeTab = 'usage'"
:class="['cursor-pointer', activeTab === 'usage' ? 'bg-blue-500 text-white' : '']" color="neutral"
variant="outline">
<p class="dark:text-white">Usage</p>
</UButton>
</div>
<div v-if="activeTab === 'usage'"
class="px-8 py-4 border border-(--border-light) dark:border-(--border-dark) rounded-md bg-(--second-light) dark:bg-(--main-dark)">
<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-white">Change Text</p>
<UIcon name="material-symbols-light:stylus-note-sharp" class="text-[30px] text-white!" />
</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>
<p ref="usageRef" v-html="productStore.productDescription.usage"
class="flex flex-col justify-center w-full text-start dark:text-white! text-black!"></p>
</div>
<div v-if="activeTab === 'description'"
class="px-8 py-4 border border-(--border-light) dark:border-(--border-dark) rounded-md bg-(--second-light) dark:bg-(--main-dark)">
<div class="flex items-center justify-end gap-3 mb-4">
<UButton v-if="!descriptionEdit.isEditing.value" @click="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="descriptionEdit.isEditing.value" @click="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="descriptionEdit.isEditing.value" @click="cancelDescriptionEdit" color="neutral"
variant="outline" class="p-2.5 cursor-pointer">Cancel</UButton>
</div>
<div ref="descriptionRef" v-html="productStore.productDescription.description"
class="flex flex-col justify-center dark:text-white text-black">
</div>
</div>
</div>
</div> </div>
</component> </component>
</template> </template>
@@ -146,175 +396,121 @@
import { ref, computed, onMounted, watch } from 'vue' import { ref, computed, onMounted, watch } from 'vue'
import { useRoute } from 'vue-router' import { useRoute } from 'vue-router'
import { useProductStore } from '@/stores/product' import { useProductStore } from '@/stores/product'
import { useSettingsStore } from '@/stores/settings'
import { langs } from '@/router/langs'
import { useEditable } from '@/composable/useConteditable' import { useEditable } from '@/composable/useConteditable'
import { langs } from '@/router/langs'
import type { Language } from '@/types'
import Default from '@/layouts/default.vue' import Default from '@/layouts/default.vue'
import { useSettingsStore } from '@/stores/settings'
const route = useRoute() const route = useRoute()
const productStore = useProductStore()
const settingsStore = useSettingsStore()
const activeTab = ref('description') const activeTab = ref('description')
const productStore = useProductStore()
const translating = ref(false) const translating = ref(false)
const noTranslation = ref(false) const settingStore = useSettingsStore()
const isEditing = ref(false)
const defaultLangId = ref(0) const availableLangs = computed(() => langs)
const selectedLangId = ref(0)
const activeLangId = ref(0) const selectedLanguage = ref(settingStore.shopDefaultLanguage)
const productID = ref(0)
const currentLangId = ref(2)
const productID = ref<number>(0)
const imageUrl = computed(() => {
return route.query.image ? String(route.query.image) : ''
})
watch(selectedLanguage, async (newLang: string) => {
if (productID.value) {
await fetchForLanguage(newLang)
}
})
const fetchForLanguage = async (langCode: string) => {
const lang = langs.find((l: Language) => l.iso_code === langCode)
if (lang && productID.value) {
await productStore.getProductDescription(lang.id, productID.value)
currentLangId.value = lang.id
}
}
const translateToSelectedLanguage = async () => {
const targetLang = langs.find((l: Language) => l.iso_code === selectedLanguage.value)
if (targetLang && currentLangId.value && productID.value) {
translating.value = true
try {
await productStore.translateProductDescription(productID.value, currentLangId.value, targetLang.id)
currentLangId.value = targetLang.id
} finally {
translating.value = false
}
}
}
onMounted(async () => {
const id = route.params.product_id
if (id) {
productID.value = Number(id)
await fetchForLanguage(selectedLanguage.value)
}
})
const descriptionRef = ref<HTMLElement | null>(null) const descriptionRef = ref<HTMLElement | null>(null)
const usageRef = ref<HTMLElement | null>(null) const usageRef = ref<HTMLElement | null>(null)
const descriptionEdit = useEditable(descriptionRef) const descriptionEdit = useEditable(descriptionRef)
const usageEdit = useEditable(usageRef) const usageEdit = useEditable(usageRef)
const isEditing = ref(false)
const originalUsage = ref('')
const originalDescription = ref('') const originalDescription = ref('')
const originalUsage = ref('')
const langOptions = computed(() => langs.filter(l => l.active !== false))
watch(selectedLangId, async (newLang) => {
if (!productID.value) return
if (newLang === defaultLangId.value) {
noTranslation.value = false
activeLangId.value = defaultLangId.value
await productStore.getProductDescription(defaultLangId.value, productID.value)
await productStore.getDefaultProductDescription(defaultLangId.value, productID.value)
return
}
await productStore.getProductDescription(newLang, productID.value)
const desc = productStore.productDescription
if (!desc || (!desc.description && !desc.description_short && !desc.usage)) {
noTranslation.value = true
activeLangId.value = defaultLangId.value
await productStore.getDefaultProductDescription(defaultLangId.value, productID.value)
} else {
noTranslation.value = false
activeLangId.value = newLang
}
})
const isViewingExistingTranslation = computed(() => activeLangId.value !== defaultLangId.value && !noTranslation.value)
const productName = computed(() => {
if (activeLangId.value !== defaultLangId.value && !noTranslation.value) {
return productStore.productDescription?.name || ''
}
return productStore.defaultProductDescription?.name || productStore.productDescription?.name || ''
})
const displayedDescription = computed(() => {
if (activeLangId.value !== defaultLangId.value && !noTranslation.value) {
return productStore.productDescription?.description || ''
}
return productStore.defaultProductDescription?.description || ''
})
const displayedShortDescription = computed(() => {
if (activeLangId.value !== defaultLangId.value && !noTranslation.value) {
return productStore.productDescription?.description_short || ''
}
return productStore.defaultProductDescription?.description_short || ''
})
const displayedUsage = computed(() => {
if (activeLangId.value !== defaultLangId.value && !noTranslation.value) {
return productStore.productDescription?.usage || ''
}
return productStore.defaultProductDescription?.usage || ''
})
const goBackToDefault = async () => {
selectedLangId.value = defaultLangId.value
activeLangId.value = defaultLangId.value
noTranslation.value = false
await productStore.getProductDescription(defaultLangId.value, productID.value)
await productStore.getDefaultProductDescription(defaultLangId.value, productID.value)
}
const translateOrSave = async () => {
if (!productID.value || selectedLangId.value === defaultLangId.value) return
translating.value = true
try {
const res = await productStore.translateProductDescription(productID.value, defaultLangId.value, selectedLangId.value)
if (res) {
activeLangId.value = selectedLangId.value
noTranslation.value = false
}
} finally {
translating.value = false
}
}
const btnClass = (tab: string) => ['cursor-pointer px-4 py-2', activeTab.value === tab ? 'bg-blue-500 text-white' : '']
/* USAGE EDIT */
const enableUsageEdit = () => {
originalUsage.value = displayedUsage.value
usageEdit.enableEdit()
isEditing.value = true
}
const saveUsage = async () => {
if (usageRef.value && productStore.productDescription) {
productStore.productDescription.usage = usageRef.value.innerHTML
}
usageEdit.disableEdit()
isEditing.value = false
await productStore.saveProductDescription(productID.value, activeLangId.value, 1)
}
const cancelUsageEdit = () => {
if (usageRef.value) usageRef.value.innerHTML = originalUsage.value
if (productStore.productDescription) productStore.productDescription.usage = originalUsage.value
usageEdit.disableEdit()
isEditing.value = false
}
/* DESCRIPTION EDIT */
const enableDescriptionEdit = () => {
originalDescription.value = displayedDescription.value
descriptionEdit.enableEdit()
}
const saveDescription = async () => { const saveDescription = async () => {
if (descriptionRef.value && productStore.productDescription) { if (descriptionRef.value) {
productStore.productDescription.description = descriptionRef.value.innerHTML productStore.productDescription.description = descriptionRef.value.innerHTML
} }
descriptionEdit.disableEdit() descriptionEdit.disableEdit()
await productStore.saveProductDescription(productID.value, activeLangId.value, 1)
await productStore.saveProductDescription(productID.value, currentLangId.value)
} }
const cancelDescriptionEdit = () => { const cancelDescriptionEdit = () => {
if (descriptionRef.value) descriptionRef.value.innerHTML = originalDescription.value if (descriptionRef.value) {
if (productStore.productDescription) productStore.productDescription.description = originalDescription.value descriptionRef.value.innerHTML = originalDescription.value
}
descriptionEdit.disableEdit() descriptionEdit.disableEdit()
} }
onMounted(async () => { const enableDescriptionEdit = () => {
const settings = await settingsStore.getSettings() if (descriptionRef.value) {
if (settings) { originalDescription.value = descriptionRef.value.innerHTML
defaultLangId.value = settings.app.shop_default_language
selectedLangId.value = defaultLangId.value
activeLangId.value = defaultLangId.value
} }
const id = route.params.product_id descriptionEdit.enableEdit()
if (id) {
productID.value = Number(id)
await productStore.getProductDescription(defaultLangId.value, productID.value)
await productStore.getDefaultProductDescription(defaultLangId.value, productID.value)
}
})
</script>
<style>
.box {
padding: 20px;
border: 1px solid var(--border-light);
border-radius: 8px;
} }
</style>
const enableEdit = () => {
if (usageRef.value) {
originalUsage.value = usageRef.value.innerHTML
}
isEditing.value = true
usageEdit.enableEdit()
}
const saveText = () => {
if (usageRef.value) {
productStore.productDescription.usage = usageRef.value.innerHTML
}
usageEdit.disableEdit()
isEditing.value = false
productStore.saveProductDescription(productID.value, currentLangId.value)
}
const cancelEdit = () => {
if (usageRef.value) {
usageRef.value.innerHTML = originalUsage.value
}
usageEdit.disableEdit()
isEditing.value = false
}
</script> -->

View File

@@ -1,29 +1,29 @@
<template> <template>
<p>Country: {{ currentCountry?.name }}</p> <div class="flex flex-col">
<p>Currency: {{ currentCountry?.ps_currency.iso_code }}</p> <p class="text-sm">Country/Currency:</p>
<USelectMenu v-model="country" :items="countries" <USelectMenu v-model="country" :items="countries"
class="w-40 bg-(--main-light) dark:bg-(--black) rounded-md shadow-sm hover:none!" valueKey="id" class="w-44 bg-(--main-light) dark:bg-(--black) rounded-md hover:none! text-sm!" valueKey="id"
:searchInput="false"> :searchInput="false">
<template #default="{ modelValue }"> <template #default="{ modelValue }">
<div class="flex items-center gap-1"> <div class="flex items-center gap-1">
<span class="font-medium dark:text-white text-black">{{ modelValue.name }}</span> <span class="font-medium dark:text-white text-black whitespace-nowrap">{{ modelValue.name }} / {{
</div> currentCountry?.ps_currency.iso_code }}</span>
</template> </div>
<template #item-leading="{ item }"> </template>
<div class="flex items-center rounded-md cursor-pointer transition-colors"> <template #item-leading="{ item }">
<span class="ml-2 dark:text-white text-black font-medium">{{ item.name }}</span> <div class="flex items-center rounded-md cursor-pointer transition-colors">
</div> <span class="ml-2 dark:text-white text-black font-medium">{{ item.name }}</span>
</template> </div>
</USelectMenu> </template>
</USelectMenu>
</div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { countries, currentCountry } from '@/router/langs' import { countries, currentCountry, switchLocalization } from '@/router/langs'
import { useRouter, useRoute } from 'vue-router' 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'
import { i18n } from '@/plugins/02_i18n'
import { useFetchJson } from '@/composable/useFetchJson'
const router = useRouter() const router = useRouter()
const route = useRoute() const route = useRoute()
@@ -34,32 +34,17 @@ const country = computed({
return currentCountry.value return currentCountry.value
}, },
set(value: string) { set(value: string) {
// i18n.locale.value = value
currentCountry.value = countries.find((x) => x.id == Number(value)) currentCountry.value = countries.find((x) => x.id == Number(value))
cookie.setCookie('country_id', `${countries.find((x) => x.id == Number(value))?.id}`, { days: 60, secure: true, sameSite: 'Lax' }) cookie.setCookie('country_id', `${countries.find((x) => x.id == Number(value))?.id}`, { days: 60, secure: true, sameSite: 'Lax' })
// changeLang() switchLocalization()
}, },
}) })
// async function changeLang() {
// try {
// const { items } = await useFetchJson('/api/v1/public/auth/update-choice', {
// method: 'POST'
// })
// } catch (error) {
// console.log(error)
// }
// }
watch( watch(
() => country, () => country,
(newCountry) => { (newCountry) => {
if (newCountry) { if (newCountry) {
console.log(newCountry.value);
// i18n.locale.value = newLocale
currentCountry.value = countries.find((x) => x.id == Number(newCountry.value?.id)) currentCountry.value = countries.find((x) => x.id == Number(newCountry.value?.id))
} }
}, },

View File

@@ -1,34 +1,37 @@
<template> <template>
<USelectMenu v-model="locale" :items="langs" <div class="flex flex-col">
class="w-40 bg-(--main-light) dark:bg-(--black) rounded-md shadow-sm hover:none!" valueKey="iso_code" <p class="text-sm">Language:</p>
:searchInput="false"> <USelectMenu v-model="locale" :items="langs"
<template #default="{ modelValue }"> class="w-40 bg-(--main-light) dark:bg-(--black) rounded-md shadow-sm hover:none!" valueKey="iso_code"
<div class="flex items-center gap-1"> :searchInput="false">
<!-- <span class="text-md dark:text-white text-black">{{langs.find(x => x.iso_code == modelValue)?.flag}}</span> --> <template #default="{ modelValue }">
<span class="font-medium dark:text-white text-black">{{langs.find(x => x.iso_code == modelValue)?.name}}</span> <div class="flex items-center gap-1">
</div> <!-- <span class="text-md dark:text-white text-black">{{langs.find(x => x.iso_code == modelValue)?.flag}}</span> -->
</template> <span class="font-medium dark:text-white text-black">{{langs.find(x => x.iso_code ==
<template #item-leading="{ item }"> modelValue)?.name}}</span>
<div class="flex items-center rounded-md cursor-pointer transition-colors"> </div>
<!-- <span class="text-md ">{{ item.flag }}</span> --> </template>
<span class="ml-2 dark:text-white text-black font-medium">{{ item.name }}</span> <template #item-leading="{ item }">
</div> <div class="flex items-center rounded-md cursor-pointer transition-colors">
</template> <!-- <span class="text-md ">{{ item.flag }}</span> -->
</USelectMenu> <span class="ml-2 dark:text-white text-black font-medium">{{ item.name }}</span>
</div>
</template>
</USelectMenu>
</div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { langs, currentLang } from '@/router/langs' import { langs, currentLang, switchLocalization } from '@/router/langs'
import { useRouter, useRoute } from 'vue-router' 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'
import { i18n } from '@/plugins/02_i18n' import { i18n } from '@/plugins/02_i18n'
import { useFetchJson } from '@/composable/useFetchJson'
const router = useRouter() const router = useRouter()
const route = useRoute() const route = useRoute()
const cookie = useCookie() const cookie = useCookie()
const locale = computed({ const locale = computed({
get() { get() {
return currentLang.value?.iso_code || i18n.locale.value return currentLang.value?.iso_code || i18n.locale.value
@@ -52,21 +55,10 @@ const locale = computed({
} }
} }
changeLang() switchLocalization()
}, },
}) })
async function changeLang() {
try {
const { items } = await useFetchJson('/api/v1/public/auth/update-choice', {
method: 'POST'
})
} catch (error) {
console.log(error)
}
}
watch( watch(
() => route.params.locale, () => route.params.locale,
(newLocale) => { (newLocale) => {

View File

@@ -1,9 +1,34 @@
<template> <template>
<UButton variant="ghost" size="sm" @click="themeStorage.setTheme()"> <UButton variant="outline" size="sm" color="info" @click="themeStorage.setTheme()">
<span class="hidden sm:inline"> <span class="hidden sm:inline">
<UIcon class="size-5" :name="themeStorage.themeIcon" /> <UIcon class="size-5" :name="themeStorage.themeIcon" />
</span> </span>
</UButton> </UButton>
<!-- <UButton variant="solid" size="sm" color="info" @click="themeStorage.setTheme()">
<span class="hidden sm:inline">
<UIcon class="size-5" :name="themeStorage.themeIcon" />
</span>
</UButton>
<UButton variant="soft" size="sm" color="info" @click="themeStorage.setTheme()">
<span class="hidden sm:inline">
<UIcon class="size-5" :name="themeStorage.themeIcon" />
</span>
</UButton>
<UButton variant="subtle" size="sm" color="info" @click="themeStorage.setTheme()">
<span class="hidden sm:inline">
<UIcon class="size-5" :name="themeStorage.themeIcon" />
</span>
</UButton>
<UButton variant="ghost" size="sm" color="info" @click="themeStorage.setTheme()">
<span class="hidden sm:inline">
<UIcon class="size-5" :name="themeStorage.themeIcon" />
</span>
</UButton>
<UButton variant="link" size="sm" color="info" @click="themeStorage.setTheme()">
<span class="hidden sm:inline">
<UIcon class="size-5" :name="themeStorage.themeIcon" />
</span>
</UButton> -->
</template> </template>
<script setup lang="ts"> <script setup lang="ts">

View File

@@ -59,7 +59,7 @@ async function setRoutes() {
const componentName = item.component const componentName = item.component
const [, folder] = componentName.split('/') const [, folder] = componentName.split('/')
const componentPath = `/src${componentName}` const componentPath = `/src${componentName}`
let modules = let modules =
folder === 'views' ? viewModules : componentModules folder === 'views' ? viewModules : componentModules
@@ -80,6 +80,8 @@ async function setRoutes() {
meta: item.meta ? JSON.parse(item.meta) : {} meta: item.meta ? JSON.parse(item.meta) : {}
}) })
} }
// await router.replace(router.currentRoute.value.fullPath)
} }
await setRoutes() await setRoutes()

View File

@@ -51,3 +51,14 @@ export async function initCountryCurrency() {
console.error('Failed to fetch languages:', error) console.error('Failed to fetch languages:', error)
} }
} }
export async function switchLocalization() {
try {
await useFetchJson('/api/v1/public/auth/update-choice', {
method: 'POST'
})
} catch (error) {
console.log(error)
}
}

View File

@@ -2,6 +2,7 @@ import { defineStore } from 'pinia'
import { ref } from 'vue' import { ref } from 'vue'
import { useFetchJson } from '@/composable/useFetchJson' import { useFetchJson } from '@/composable/useFetchJson'
import type { ProductDescription } from '@/types/product' import type { ProductDescription } from '@/types/product'
import { useSettingsStore } from './settings'
export interface Product { export interface Product {
id: number id: number
@@ -23,21 +24,20 @@ export interface ProductResponse {
} }
export const useProductStore = defineStore('product', () => { export const useProductStore = defineStore('product', () => {
const productDescription = ref<ProductDescription | null>(null)
const defaultProductDescription = ref<ProductDescription | null>(null)
const currentProduct = ref<Product | null>(null)
const loading = ref(false) const loading = ref(false)
const error = ref<string | null>(null) const error = ref<string | null>(null)
const productDescription = ref()
async function getProductDescription(langId = 1, productID: number) { async function getProductDescription(langId: number, productID: number) {
loading.value = true loading.value = true
error.value = null error.value = null
try { try {
const response = await useFetchJson<ProductDescription>( const response = await useFetchJson<ProductDescription>(
`/api/v1/restricted/product-translation/get-product-description?productID=${productID}&productLangID=${langId}` `/api/v1/restricted/product-translation/get-product-description?productID=${productID}&productLangID=${langId}`
) )
productDescription.value = response.items productDescription.value = response.items
console.log(productDescription, 'dfsfsdf');
} 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,45 +45,12 @@ export const useProductStore = defineStore('product', () => {
} }
} }
async function getDefaultProductDescription(langId: number, productID: number) { async function translateProductDescription(productID: number, toLangId: number, defaultLangId: number, model: string = 'OpenAI') {
try {
const response = await useFetchJson<ProductDescription>(
`/api/v1/restricted/product-translation/get-product-description?productID=${productID}&productLangID=${langId}`
)
defaultProductDescription.value = response.items
} catch (e: unknown) {
console.error('Failed to load default product description:', e)
}
}
async function saveProductDescription(productID?: number, langId?: number, shopId = 1) {
const id = productID || 1
try {
const data = await useFetchJson(
`/api/v1/restricted/product-description/save-product-description?productID=${id}&productShopID=${shopId}&productLangID=${langId ?? 1}`,
{
method: 'POST',
body: JSON.stringify({
description: productDescription.value?.description,
description_short: productDescription.value?.description_short,
meta_description: productDescription.value?.meta_description,
available_now: productDescription.value?.available_now,
usage: productDescription.value?.usage
})
}
)
return data
} catch (e) {
console.error(e)
}
}
async function translateProductDescription(productID: number, fromLangId: number, toLangId: number) {
loading.value = true loading.value = true
error.value = null error.value = null
try { try {
const response = await useFetchJson<ProductDescription>(`/api/v1/restricted/product-description/translate-product-description?productID=${productID}&productShopID=1&productFromLangID=${fromLangId}&productToLangID=${toLangId}&model=OpenAI`) const response = await useFetchJson<ProductDescription>(`/api/v1/restricted/product-translation/translate-product-description?productID=${productID}&productFromLangID=${defaultLangId}&productToLangID=${toLangId}&model=${model}`)
productDescription.value = response.items productDescription.value = response.items
return response.items return response.items
} catch (e: any) { } catch (e: any) {
@@ -94,20 +61,166 @@ export const useProductStore = defineStore('product', () => {
} }
} }
function clearCurrentProduct() {
currentProduct.value = null function stripHtml(html: string) {
const div = document.createElement('div')
div.innerHTML = html
return div.textContent || div.innerText || ''
}
async function saveProductDescription(productID?: number, langId?: number) {
const id = productID || 1
const lang = langId || 1
try {
const data = await useFetchJson(
`/api/v1/restricted/product-translation/save-product-description?productID=${id}&productLangID=${lang}`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
name: stripHtml(productDescription.value?.name || ''),
description: stripHtml(productDescription.value?.description || ''),
description_short: stripHtml(productDescription.value?.description_short || ''),
meta_title: stripHtml(productDescription.value?.meta_title || ''),
meta_description: stripHtml(productDescription.value?.meta_description || ''),
available_now: stripHtml(productDescription.value?.available_now || ''),
available_later: stripHtml(productDescription.value?.available_later || ''),
usage: stripHtml(productDescription.value?.usage || '')
})
}
)
return data
} catch (e) {
console.error(e)
}
} }
return { return {
productDescription, productDescription,
defaultProductDescription,
currentProduct,
loading, loading,
error, error,
getProductDescription,
getDefaultProductDescription,
clearCurrentProduct,
saveProductDescription,
translateProductDescription, translateProductDescription,
getProductDescription,
saveProductDescription
} }
}) })
// import { defineStore } from 'pinia'
// import { ref } from 'vue'
// import { useFetchJson } from '@/composable/useFetchJson'
// import type { ProductDescription } from '@/types/product'
// import { useSettingsStore } from './settings'
// export interface Product {
// id: number
// image: string
// name: string
// code: string
// inStock: boolean
// priceFrom: number
// priceTo: number
// count: number
// description?: string
// howToUse?: string
// productDetails?: string
// }
// export interface ProductResponse {
// items: Product[]
// items_count: number
// }
// export const useProductStore = defineStore('product', () => {
// const productDescription = ref()
// const currentProduct = ref<Product | null>(null)
// const loading = ref(false)
// const error = ref<string | null>(null)
// async function getProductDescription(langId = 1, productID: number) {
// loading.value = true
// error.value = null
// try {
// const response = await useFetchJson<ProductDescription>(
// `/api/v1/restricted/product-translation/get-product-description?productID=${productID}&productLangID=${langId}`
// )
// productDescription.value = response.items
// console.log(productDescription, 'dfsfsdf');
// } catch (e: unknown) {
// error.value = e instanceof Error ? e.message : 'Failed to load product description'
// } finally {
// loading.value = false
// }
// }
// function stripHtml(html: string) {
// const div = document.createElement('div')
// div.innerHTML = html
// return div.textContent || div.innerText || ''
// }
// async function saveProductDescription(productID?: number, langId?: number) {
// const id = productID || 1
// const lang = langId || 1
// try {
// const data = await useFetchJson(
// `/api/v1/restricted/product-translation/save-product-description?productID=${id}&productLangID=${lang}`,
// {
// method: 'POST',
// headers: {
// 'Content-Type': 'application/json'
// },
// body: JSON.stringify({
// name: stripHtml(productDescription.value?.name || ''),
// description: stripHtml(productDescription.value?.description || ''),
// description_short: stripHtml(productDescription.value?.description_short || ''),
// meta_title: stripHtml(productDescription.value?.meta_title || ''),
// meta_description: stripHtml(productDescription.value?.meta_description || ''),
// available_now: stripHtml(productDescription.value?.available_now || ''),
// available_later: stripHtml(productDescription.value?.available_later || ''),
// usage: stripHtml(productDescription.value?.usage || '')
// })
// }
// )
// return data
// } catch (e) {
// console.error(e)
// }
// }
// const defaultLangId = ref(1)
// async function translateProductDescription(productID: number, fromLangId: number, defaultLangId: number, model: string = 'OpenAI') {
// loading.value = true
// error.value = null
// try {
// const response = await useFetchJson<ProductDescription>(`/api/v1/restricted/product-translation/translate-product-description?productID=${productID}&productFromLangID=${fromLangId}&productToLangID=${defaultLangId}&model=${model}`)
// productDescription.value = response.items
// return response.items
// } catch (e: any) {
// error.value = e?.message || 'Failed to translate product description'
// console.error('Failed to translate product description:', e)
// } finally {
// loading.value = false
// }
// }
// function clearCurrentProduct() {
// currentProduct.value = null
// }
// return {
// productDescription,
// currentProduct,
// loading,
// error,
// getProductDescription,
// clearCurrentProduct,
// saveProductDescription,
// translateProductDescription,
// }
// })

View File

@@ -2,11 +2,12 @@ import { useFetchJson } from '@/composable/useFetchJson'
import type { Resp } from '@/types' import type { Resp } from '@/types'
import type { Settings } from '@/types/settings' import type { Settings } from '@/types/settings'
import { defineStore } from 'pinia' import { defineStore } from 'pinia'
import { ref } from 'vue' import { computed, ref } from 'vue'
export const useSettingsStore = defineStore('settings', () => { export const useSettingsStore = defineStore('settings', () => {
const settings = ref<Settings | null>(null) const settings = ref<Settings | null>(null)
const loaded = ref(false) const loaded = ref(false)
const shopDefaultLanguage = computed(() => settings.value?.app?.shop_default_language ?? 1)
async function getSettings(): Promise<Settings | null> { async function getSettings(): Promise<Settings | null> {
if (loaded.value && settings.value) return settings.value if (loaded.value && settings.value) return settings.value
@@ -16,5 +17,5 @@ export const useSettingsStore = defineStore('settings', () => {
return resp.items return resp.items
} }
return { settings, loaded, getSettings } return { settings, loaded, shopDefaultLanguage, getSettings }
}) })