Compare commits
1 Commits
currencies
...
front-styl
| Author | SHA1 | Date | |
|---|---|---|---|
| 564656dcd6 |
4
bo/components.d.ts
vendored
4
bo/components.d.ts
vendored
@@ -11,6 +11,7 @@ export {}
|
||||
/* prettier-ignore */
|
||||
declare module 'vue' {
|
||||
export interface GlobalComponents {
|
||||
'>': typeof import('./src/components/admin/product/ <TabGeneralSkeleton v-if="addProductStore.loadingCategories" />.vue')['default']
|
||||
AddProduct: typeof import('./src/components/admin/AddProduct.vue')['default']
|
||||
CartDetails: typeof import('./src/components/customer/CartDetails.vue')['default']
|
||||
CategoryMenu: typeof import('./src/components/inner/CategoryMenu.vue')['default']
|
||||
@@ -22,6 +23,7 @@ declare module 'vue' {
|
||||
En_TermsAndConditionsView: typeof import('./src/components/terms/en_TermsAndConditionsView.vue')['default']
|
||||
FavoriteProducts: typeof import('./src/components/admin/FavoriteProducts.vue')['default']
|
||||
LangSwitch: typeof import('./src/components/inner/LangSwitch.vue')['default']
|
||||
LayoutSkeleton: typeof import('./src/components/ui/LayoutSkeleton.vue')['default']
|
||||
PageAddresses: typeof import('./src/components/customer/PageAddresses.vue')['default']
|
||||
PageCart: typeof import('./src/components/customer/PageCart.vue')['default']
|
||||
PageCarts: typeof import('./src/components/customer/PageCarts.vue')['default']
|
||||
@@ -47,6 +49,8 @@ declare module 'vue' {
|
||||
RouterView: typeof import('vue-router')['RouterView']
|
||||
StorageFileBrowser: typeof import('./src/components/customer/StorageFileBrowser.vue')['default']
|
||||
TabGeneral: typeof import('./src/components/admin/product/TabGeneral.vue')['default']
|
||||
TabGeneralSceleton: typeof import('./src/components/admin/product/TabGeneralSceleton.vue')['default']
|
||||
TabGeneralSkeleton: typeof import('./src/components/admin/product/TabGeneralSkeleton.vue')['default']
|
||||
TabOptions: typeof import('./src/components/admin/product/TabOptions.vue')['default']
|
||||
TabPricing: typeof import('./src/components/admin/product/TabPricing.vue')['default']
|
||||
TabQuantities: typeof import('./src/components/admin/product/TabQuantities.vue')['default']
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
<template>
|
||||
<div class="space-y-5">
|
||||
{{ addProductStore.form }}
|
||||
<TabGeneralSkeleton v-if="addProductStore.loadingCategories" />
|
||||
|
||||
<div v-else class="space-y-5">
|
||||
<!-- {{ addProductStore.form }} -->
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<UFormField label="Product name" name="productName">
|
||||
@@ -23,7 +25,6 @@
|
||||
<fieldset
|
||||
class="rounded-xl border border-(--border-light) dark:border-(--border-dark) p-4 space-y-3 bg-white dark:bg-neutral-900">
|
||||
<legend class="px-1 block font-medium text-default text-[16px]">Images</legend>
|
||||
{{ addProductStore.images }}
|
||||
|
||||
<div class="flex items-start gap-3 flex-wrap">
|
||||
<div v-for="(img, idx) in addProductStore.images" :key="idx" class="relative group shrink-0">
|
||||
@@ -78,27 +79,25 @@
|
||||
</fieldset>
|
||||
|
||||
<div class="space-y-2">
|
||||
<p class="text-sm font-medium text-black dark:text-white">Powiązany produkt</p>
|
||||
<legend class="px-1 block font-medium text-default text-[16px]">Powiązany produkt</legend>
|
||||
|
||||
<UInput v-model="relatedSearch" placeholder="Search and add Powiązany produkt" icon="i-lucide-search"
|
||||
class="w-full" />
|
||||
|
||||
<div v-if="addProductStore.relatedProducts.length > 0"
|
||||
class="rounded-lg border border-(--border-light) dark:border-(--border-dark) overflow-hidden">
|
||||
<div v-for="p in addProductStore.relatedProducts" :key="p.id_product"
|
||||
class="flex items-center justify-between px-3 py-2 text-sm text-black dark:text-white border-b border-(--border-light) dark:border-(--border-dark) last:border-0">
|
||||
<div v-for="p in addProductStore.relatedProducts" :key="p.product_id"
|
||||
class="flex items-center justify-between px-3 py-2 text-sm text-black dark:text-white border-b border-(--border-light) dark:border-(--border-dark) last:border-0 bg-white">
|
||||
<span>{{ p.name }}</span>
|
||||
<button type="button" @click="removeRelated(p.id_product)"
|
||||
class="text-gray-400 hover:text-red-500 transition-colors ml-2">
|
||||
<button type="button" @click="removeRelated(p.product_id)"
|
||||
class="text-red-500 transition-colors ml-2 cursor-pointer hover:text-red-700">
|
||||
<UIcon name="i-lucide-x" class="text-base" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<USelect :items="relatedResults" value-key="product_id" />
|
||||
<div v-if="relatedResults.length > 0"
|
||||
class="rounded-lg border border-(--border-light) dark:border-(--border-dark) overflow-hidden bg-white dark:bg-neutral-900">
|
||||
<button v-for="p in relatedResults" :key="p.id_product" type="button"
|
||||
class="rounded-lg border border-(--border-light) dark:border-(--border-dark) bg-white dark:bg-neutral-900 max-h-80 overflow-auto">
|
||||
<button v-for="p in relatedResults" :key="p.product_id" type="button"
|
||||
class="w-full text-left px-3 py-2 text-sm text-black dark:text-white hover:bg-gray-50 dark:hover:bg-neutral-800 transition-colors border-b border-(--border-light) dark:border-(--border-dark) last:border-0"
|
||||
@click="addRelated(p)">
|
||||
{{ p.name }}
|
||||
@@ -127,14 +126,12 @@
|
||||
|
||||
<div class="space-y-0.5 overflow-y-auto flex">
|
||||
<UNavigationMenu orientation="vertical" type="multiple" :items="filteredCategories" :ui="{
|
||||
root: 'w-auto'
|
||||
root: 'w-auto max-h-80'
|
||||
}">
|
||||
<template #item="{ item }">
|
||||
<div
|
||||
class="flex items-center gap-2.5 cursor-pointer rounded px-1.5 py-1 hover:bg-gray-50 dark:hover:bg-neutral-800 transition-colors"
|
||||
<div class="flex items-center gap-2.5 cursor-pointer rounded px-1.5 py-1 hover:bg-gray-50 dark:hover:bg-neutral-800 transition-colors"
|
||||
@click.stop="toggleCategory({ id_category: item.params?.category_id, name: item.label as string })">
|
||||
<UCheckbox :model-value="isCategorySelected(item.params?.category_id)"
|
||||
color="info" />
|
||||
<UCheckbox :model-value="isCategorySelected(item.params?.category_id)" color="info" />
|
||||
<span class="text-sm text-black dark:text-white">{{ item.label }}</span>
|
||||
</div>
|
||||
</template>
|
||||
@@ -156,6 +153,7 @@ import errorImg from '@/assets/error.svg'
|
||||
import { useFetchJson } from '@/composable/useFetchJson'
|
||||
import { watch } from 'vue'
|
||||
import type { NavigationMenuItem } from '@nuxt/ui'
|
||||
import TabGeneralSkeleton from './TabGeneralSkeleton.vue'
|
||||
|
||||
const addProductStore = useAddProductStore()
|
||||
|
||||
@@ -185,12 +183,6 @@ function adaptCategory(menu: NavigationMenuItem[]) {
|
||||
if (item.children && item.children.length > 0) {
|
||||
item.open = true
|
||||
adaptCategory(item.children);
|
||||
|
||||
item.children.unshift({
|
||||
label: item.label,
|
||||
icon: 'i-lucide-book-open',
|
||||
params: item.params,
|
||||
})
|
||||
} else {
|
||||
item.icon = 'i-lucide-file-text'
|
||||
}
|
||||
@@ -198,7 +190,7 @@ function adaptCategory(menu: NavigationMenuItem[]) {
|
||||
return menu;
|
||||
}
|
||||
|
||||
//there have to be request on filter category (but have to return like parent => children =>)
|
||||
//filter category - return like parent => children
|
||||
const filteredCategories = computed(() => {
|
||||
const q = categorySearch.value.trim().toLowerCase()
|
||||
if (!q) return allCategories.value
|
||||
@@ -210,8 +202,6 @@ function isCategorySelected(id: number) {
|
||||
}
|
||||
|
||||
function toggleCategory(cat: ProductCategory) {
|
||||
console.log(cat);
|
||||
|
||||
if (isCategorySelected(cat.id_category)) {
|
||||
removeCategory(cat.id_category)
|
||||
} else {
|
||||
@@ -238,7 +228,7 @@ watch(relatedSearch, (q) => {
|
||||
})
|
||||
|
||||
function addRelated(product: ProductRelated) {
|
||||
if (!addProductStore.relatedProducts.some(p => p.id_product === product.id_product)) {
|
||||
if (!addProductStore.relatedProducts.some(p => p.product_id === product.product_id)) {
|
||||
addProductStore.relatedProducts.push(product)
|
||||
}
|
||||
relatedSearch.value = ''
|
||||
@@ -246,7 +236,7 @@ function addRelated(product: ProductRelated) {
|
||||
}
|
||||
|
||||
function removeRelated(id: number) {
|
||||
const idx = addProductStore.relatedProducts.findIndex(p => p.id_product === id)
|
||||
const idx = addProductStore.relatedProducts.findIndex(p => p.product_id === id)
|
||||
if (idx !== -1) addProductStore.relatedProducts.splice(idx, 1)
|
||||
}
|
||||
|
||||
@@ -274,5 +264,5 @@ async function fetchProducts(q: string) {
|
||||
}
|
||||
}
|
||||
|
||||
await addProductStore.loadCategories()
|
||||
addProductStore.loadCategories()
|
||||
</script>
|
||||
|
||||
52
bo/src/components/admin/product/TabGeneralSkeleton.vue
Normal file
52
bo/src/components/admin/product/TabGeneralSkeleton.vue
Normal file
@@ -0,0 +1,52 @@
|
||||
<template>
|
||||
<div class="space-y-5 animate-pulse">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div class="space-y-1.5">
|
||||
<div class="h-3.5 w-24 rounded bg-gray-200 dark:bg-neutral-700" />
|
||||
<div class="h-9 rounded-lg bg-gray-200 dark:bg-neutral-700 w-full" />
|
||||
</div>
|
||||
<div class="space-y-1.5">
|
||||
<div class="h-3.5 w-28 rounded bg-gray-200 dark:bg-neutral-700" />
|
||||
<div class="h-9 rounded-lg bg-gray-200 dark:bg-neutral-700 w-full" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div class="space-y-1.5">
|
||||
<div class="h-3.5 w-20 rounded bg-gray-200 dark:bg-neutral-700" />
|
||||
<div class="h-32 rounded-lg bg-gray-200 dark:bg-neutral-700 w-full" />
|
||||
</div>
|
||||
<div class="space-y-1.5">
|
||||
<div class="h-3.5 w-20 rounded bg-gray-200 dark:bg-neutral-700" />
|
||||
<div class="h-32 rounded-lg bg-gray-200 dark:bg-neutral-700 w-full" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rounded-xl border border-(--border-light) dark:border-(--border-dark) p-4 space-y-3">
|
||||
<div class="h-4 w-16 rounded bg-gray-200 dark:bg-neutral-700" />
|
||||
<div class="flex gap-3">
|
||||
<div v-for="i in 3" :key="i" class="w-36 h-36 rounded-xl bg-gray-200 dark:bg-neutral-700 shrink-0" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<div class="h-3.5 w-32 rounded bg-gray-200 dark:bg-neutral-700" />
|
||||
<div class="h-9 rounded-lg bg-gray-200 dark:bg-neutral-700 w-full" />
|
||||
</div>
|
||||
|
||||
<div class="rounded-xl border border-(--border-light) dark:border-(--border-dark) p-4 space-y-3">
|
||||
<div class="h-4 w-20 rounded bg-gray-200 dark:bg-neutral-700" />
|
||||
<div class="h-9 w-52 rounded-lg bg-gray-200 dark:bg-neutral-700" />
|
||||
<div class="space-y-2">
|
||||
<div v-for="i in 5" :key="i" class="flex items-center gap-2.5 px-1.5 py-1">
|
||||
<div class="h-4 w-4 rounded bg-gray-200 dark:bg-neutral-700 shrink-0" />
|
||||
<div class="h-3.5 rounded bg-gray-200 dark:bg-neutral-700"
|
||||
:class="['w-32', 'w-48', 'w-40', 'w-36', 'w-44'][i - 1]" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
</script>
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<UEditor v-slot="{ editor }" v-model="localValue" content-type="html"
|
||||
:ui="{ base: 'p-8 sm:px-16', root: 'p-2' }"
|
||||
:ui="{ base: 'p-8', 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>
|
||||
|
||||
52
bo/src/components/ui/LayoutSkeleton.vue
Normal file
52
bo/src/components/ui/LayoutSkeleton.vue
Normal file
@@ -0,0 +1,52 @@
|
||||
<template>
|
||||
<div class="flex flex-1 overflow-x-hidden h-svh animate-pulse">
|
||||
<!-- Sidebar -->
|
||||
<div class="flex flex-col shrink-0 w-52 bg-elevated/25 border-r border-default h-full">
|
||||
<!-- Sidebar header -->
|
||||
<div class="flex items-center gap-2 p-3 border-b border-default h-(--ui-header-height)">
|
||||
<div class="h-8 w-8 rounded-lg bg-gray-200 dark:bg-neutral-700 shrink-0" />
|
||||
<div class="h-4 flex-1 rounded bg-gray-200 dark:bg-neutral-700" />
|
||||
</div>
|
||||
|
||||
<!-- Sidebar nav items -->
|
||||
<div class="flex flex-col gap-1 p-2 flex-1">
|
||||
<div v-for="i in 7" :key="i" class="flex items-center gap-2.5 px-1.5 py-1.5">
|
||||
<div class="h-5 w-5 rounded bg-gray-200 dark:bg-neutral-700 shrink-0" />
|
||||
<div class="h-3.5 rounded bg-gray-200 dark:bg-neutral-700" :class="['w-20','w-28','w-24','w-16','w-28','w-20','w-24'][i-1]" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Sidebar footer -->
|
||||
<div class="p-3 border-t border-default">
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="h-8 w-8 rounded-lg bg-gray-200 dark:bg-neutral-700 shrink-0" />
|
||||
<div class="h-3.5 flex-1 rounded bg-gray-200 dark:bg-neutral-700" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Main area -->
|
||||
<div class="flex-1 flex flex-col overflow-hidden">
|
||||
<!-- Header -->
|
||||
<div class="flex h-(--ui-header-height) shrink-0 items-center justify-between px-4 border-b border-default">
|
||||
<!-- Left: toggle + title -->
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="h-8 w-8 rounded-lg bg-gray-200 dark:bg-neutral-700" />
|
||||
<div class="h-5 w-36 rounded bg-gray-200 dark:bg-neutral-700" />
|
||||
</div>
|
||||
<!-- Right: controls -->
|
||||
<div class="hidden md:flex items-center gap-3">
|
||||
<div class="h-8 w-20 rounded-lg bg-gray-200 dark:bg-neutral-700" />
|
||||
<div class="h-8 w-20 rounded-lg bg-gray-200 dark:bg-neutral-700" />
|
||||
<div class="h-8 w-8 rounded-lg bg-gray-200 dark:bg-neutral-700" />
|
||||
<div class="h-8 w-24 rounded-lg bg-gray-200 dark:bg-neutral-700" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Page content placeholder -->
|
||||
<div class="flex-1 p-4 bg-slate-50 dark:bg-(--main-dark)">
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,5 +1,7 @@
|
||||
<template>
|
||||
<div class="flex flex-1 overflow-x-hidden h-svh">
|
||||
<LayoutSkeleton v-if="loadingLayout" />
|
||||
|
||||
<div v-else class="flex flex-1 overflow-x-hidden h-svh">
|
||||
<USidebar v-model:open="open" collapsible="icon" rail :ui="{
|
||||
container: 'h-full z-80',
|
||||
inner: 'bg-elevated/25 divide-transparent',
|
||||
@@ -88,6 +90,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import LayoutSkeleton from '@/components/ui/LayoutSkeleton.vue'
|
||||
import { useColorMode } from '@vueuse/core'
|
||||
import type { DropdownMenuItem, NavigationMenuItem } from '@nuxt/ui'
|
||||
import { defineShortcuts, extractShortcuts } from '@nuxt/ui/runtime/composables/defineShortcuts.js'
|
||||
@@ -98,7 +101,9 @@ const userStore = useUserStore()
|
||||
|
||||
const route = useRoute()
|
||||
const pageTitle = computed(() => route.meta.name ?? 'Default Page')
|
||||
await userStore.getUser()
|
||||
|
||||
const loadingLayout = ref(true)
|
||||
userStore.getUser().finally(() => { loadingLayout.value = false })
|
||||
|
||||
const open = ref(true)
|
||||
const colorMode = useColorMode()
|
||||
|
||||
@@ -182,7 +182,7 @@ const menu = ref<TopMenuItem[] | null>(null)
|
||||
const Id = Number(route.params.user_id)
|
||||
async function cmGetTopMenu() {
|
||||
try {
|
||||
const { items } = await useFetchJson<TopMenuItem[]>(`/api/v1/restricted/menu/get-top-menu?target_user_id=${Id}`)
|
||||
const { items } = await useFetchJson<TopMenuItem[]>(`/api/v1/restricted/menu/get-customer-management-menu`)
|
||||
|
||||
menu.value = items
|
||||
} catch (err) {
|
||||
|
||||
@@ -136,9 +136,12 @@ export const useAddProductStore = defineStore('addProduct', () => {
|
||||
}
|
||||
|
||||
const categories = ref<MenuItem[]>([])
|
||||
const loadingCategories = ref(true)
|
||||
async function loadCategories() {
|
||||
loadingCategories.value = true
|
||||
const resp = await useFetchJson<MenuItem>(`/api/v1/restricted/menu/get-category-tree?root_category_id=${settings['app'].category_tree_root_id}`);
|
||||
categories.value = resp.items.children
|
||||
loadingCategories.value = false
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -154,6 +157,7 @@ export const useAddProductStore = defineStore('addProduct', () => {
|
||||
variantSaving,
|
||||
variantErrors,
|
||||
categories,
|
||||
loadingCategories,
|
||||
loadProduct,
|
||||
addImageFiles,
|
||||
removeImage,
|
||||
|
||||
@@ -11,12 +11,8 @@ export const useUserStore = defineStore('user', () => {
|
||||
error.value = null
|
||||
try {
|
||||
const data = await useFetchJson<User>(`/api/v1/restricted/customer`)
|
||||
console.log('getUser API response:', data)
|
||||
|
||||
const response: User = (data as any).items ?? data
|
||||
console.log('User response:', response)
|
||||
user.value = response
|
||||
|
||||
return response
|
||||
} catch (err: any) {
|
||||
error.value = err?.message ?? 'Unknown error'
|
||||
|
||||
2
bo/src/types/product.d.ts
vendored
2
bo/src/types/product.d.ts
vendored
@@ -25,7 +25,7 @@ export interface ProductCategory {
|
||||
}
|
||||
|
||||
export interface ProductRelated {
|
||||
id_product: number
|
||||
product_id: number
|
||||
name: string
|
||||
reference: string
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user