fix: added page PageProductCardFull and Addresses
This commit is contained in:
10
bo/components.d.ts
vendored
10
bo/components.d.ts
vendored
@@ -16,8 +16,16 @@ declare module 'vue' {
|
|||||||
En_PrivacyPolicyView: typeof import('./src/components/terms/en_PrivacyPolicyView.vue')['default']
|
En_PrivacyPolicyView: typeof import('./src/components/terms/en_PrivacyPolicyView.vue')['default']
|
||||||
En_TermsAndConditionsView: typeof import('./src/components/terms/en_TermsAndConditionsView.vue')['default']
|
En_TermsAndConditionsView: typeof import('./src/components/terms/en_TermsAndConditionsView.vue')['default']
|
||||||
LangSwitch: typeof import('./src/components/inner/langSwitch.vue')['default']
|
LangSwitch: typeof import('./src/components/inner/langSwitch.vue')['default']
|
||||||
|
PageAddresses: typeof import('./src/components/customer/PageAddresses.vue')['default']
|
||||||
|
PageProductCardFull: typeof import('./src/components/customer/PageProductCardFull.vue')['default']
|
||||||
Pl_PrivacyPolicyView: typeof import('./src/components/terms/pl_PrivacyPolicyView.vue')['default']
|
Pl_PrivacyPolicyView: typeof import('./src/components/terms/pl_PrivacyPolicyView.vue')['default']
|
||||||
Pl_TermsAndConditionsView: typeof import('./src/components/terms/pl_TermsAndConditionsView.vue')['default']
|
Pl_TermsAndConditionsView: typeof import('./src/components/terms/pl_TermsAndConditionsView.vue')['default']
|
||||||
|
ProductCardFull: typeof import('./src/components/customer/ProductCardFull.vue')['default']
|
||||||
|
ProductCustomization: typeof import('./src/components/customer/components/ProductCustomization.vue')['default']
|
||||||
|
ProductDetailView: typeof import('./src/components/admin/ProductDetailView.vue')['default']
|
||||||
|
ProductsView: typeof import('./src/components/admin/ProductsView.vue')['default']
|
||||||
|
ProductVariants: typeof import('./src/components/customer/components/ProductVariants.vue')['default']
|
||||||
|
'ProductСustomization': typeof import('./src/components/customer/components/ProductСustomization.vue')['default']
|
||||||
RouterLink: typeof import('vue-router')['RouterLink']
|
RouterLink: typeof import('vue-router')['RouterLink']
|
||||||
RouterView: typeof import('vue-router')['RouterView']
|
RouterView: typeof import('vue-router')['RouterView']
|
||||||
ThemeSwitch: typeof import('./src/components/inner/themeSwitch.vue')['default']
|
ThemeSwitch: typeof import('./src/components/inner/themeSwitch.vue')['default']
|
||||||
@@ -32,6 +40,8 @@ declare module 'vue' {
|
|||||||
UFormField: typeof import('./node_modules/@nuxt/ui/dist/runtime/components/FormField.vue')['default']
|
UFormField: typeof import('./node_modules/@nuxt/ui/dist/runtime/components/FormField.vue')['default']
|
||||||
UIcon: typeof import('./node_modules/@nuxt/ui/dist/runtime/vue/components/Icon.vue')['default']
|
UIcon: typeof import('./node_modules/@nuxt/ui/dist/runtime/vue/components/Icon.vue')['default']
|
||||||
UInput: typeof import('./node_modules/@nuxt/ui/dist/runtime/components/Input.vue')['default']
|
UInput: typeof import('./node_modules/@nuxt/ui/dist/runtime/components/Input.vue')['default']
|
||||||
|
UInputNumber: typeof import('./node_modules/@nuxt/ui/dist/runtime/components/InputNumber.vue')['default']
|
||||||
|
UModal: typeof import('./node_modules/@nuxt/ui/dist/runtime/components/Modal.vue')['default']
|
||||||
UPagination: typeof import('./node_modules/@nuxt/ui/dist/runtime/components/Pagination.vue')['default']
|
UPagination: typeof import('./node_modules/@nuxt/ui/dist/runtime/components/Pagination.vue')['default']
|
||||||
USelect: typeof import('./node_modules/@nuxt/ui/dist/runtime/components/Select.vue')['default']
|
USelect: typeof import('./node_modules/@nuxt/ui/dist/runtime/components/Select.vue')['default']
|
||||||
USelectMenu: typeof import('./node_modules/@nuxt/ui/dist/runtime/components/SelectMenu.vue')['default']
|
USelectMenu: typeof import('./node_modules/@nuxt/ui/dist/runtime/components/SelectMenu.vue')['default']
|
||||||
|
|||||||
@@ -22,6 +22,13 @@ export const uiOptions: NuxtUIOptions = {
|
|||||||
error: 'text-red-600!'
|
error: 'text-red-600!'
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
inputNumber: {
|
||||||
|
slots: {
|
||||||
|
base: 'text-(--black) dark:text-white border! border-(--border-light)! dark:border-(--border-dark)! outline-0! ring-0! pt-2 px-1! w-auto!',
|
||||||
|
increment: 'border-0! pe-0! ps-0!',
|
||||||
|
decrement: 'border-0! pe-0! ps-0!'
|
||||||
|
},
|
||||||
|
},
|
||||||
select: {
|
select: {
|
||||||
slots: {
|
slots: {
|
||||||
base: 'w-full! cursor-pointer border! border-(--border-light)! dark:border-(--border-dark)! outline-0! ring-0!',
|
base: 'w-full! cursor-pointer border! border-(--border-light)! dark:border-(--border-dark)! outline-0! ring-0!',
|
||||||
@@ -43,7 +50,7 @@ export const uiOptions: NuxtUIOptions = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
},
|
},
|
||||||
table: {
|
table: {
|
||||||
slots: {
|
slots: {
|
||||||
base: 'border! border-(--border-light)! dark:border-(--border-dark)! outline-0! ring-0! bg-(--second-light) dark:bg-(--main-dark)',
|
base: 'border! border-(--border-light)! dark:border-(--border-dark)! outline-0! ring-0! bg-(--second-light) dark:bg-(--main-dark)',
|
||||||
tr: 'border-b! border-(--border-light)! dark:border-(--border-dark)! outline-0! ring-0! text-(--black)! dark:text-white!',
|
tr: 'border-b! border-(--border-light)! dark:border-(--border-dark)! outline-0! ring-0! text-(--black)! dark:text-white!',
|
||||||
|
|||||||
@@ -22,6 +22,12 @@ const authStore = useAuthStore()
|
|||||||
<RouterLink :to="{ name: 'product-detail' }">
|
<RouterLink :to="{ name: 'product-detail' }">
|
||||||
product detail
|
product detail
|
||||||
</RouterLink>
|
</RouterLink>
|
||||||
|
<RouterLink :to="{ name: 'product-card-full' }">
|
||||||
|
ProductCardFull
|
||||||
|
</RouterLink>
|
||||||
|
<RouterLink :to="{ name: 'addresses' }">
|
||||||
|
Addresses
|
||||||
|
</RouterLink>
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<!-- Language Switcher -->
|
<!-- Language Switcher -->
|
||||||
<LangSwitch />
|
<LangSwitch />
|
||||||
|
|||||||
154
bo/src/components/customer/PageAddresses.vue
Normal file
154
bo/src/components/customer/PageAddresses.vue
Normal file
@@ -0,0 +1,154 @@
|
|||||||
|
<template>
|
||||||
|
<div class="container">
|
||||||
|
<div class="p-6 bg-white dark:bg-(--black) min-h-screen font-sans">
|
||||||
|
<div class="flex justify-between items-center mb-6">
|
||||||
|
<h1 class="text-2xl font-bold text-black dark:text-white">
|
||||||
|
{{ t('addresses.title') }}
|
||||||
|
</h1>
|
||||||
|
<UButton @click="showModal = true"
|
||||||
|
class="px-4 py-2 bg-primary text-white rounded-lg hover:bg-blue-600 transition-colors flex items-center gap-2">
|
||||||
|
<span class="i-lucide-plus" />
|
||||||
|
{{ t('addresses.addNew') }}
|
||||||
|
</UButton>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<div class="mb-6">
|
||||||
|
<UInput v-model="searchQuery" type="text" :placeholder="t('addresses.searchPlaceholder')"
|
||||||
|
class="bg-white dark:bg-gray-800 text-black dark:text-white w-[30%]" />
|
||||||
|
</div>
|
||||||
|
<div v-if="paginatedAddresses.length > 0" class="space-y-4">
|
||||||
|
<div v-for="address in paginatedAddresses" :key="address.id"
|
||||||
|
class="border border-gray-200 dark:border-gray-700 rounded-lg p-4 bg-white dark:bg-gray-800 hover:shadow-md transition-shadow">
|
||||||
|
<div class="flex justify-between items-start">
|
||||||
|
<div class="flex-1">
|
||||||
|
<p class="text-black dark:text-white mt-1">
|
||||||
|
{{ address.street }}
|
||||||
|
</p>
|
||||||
|
<p class="text-black dark:text-white flex gap-2">
|
||||||
|
{{ address.zipCode }}, <span>{{ address.city }}</span>
|
||||||
|
</p>
|
||||||
|
<p class="text-black dark:text-white">{{ address.country }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-else class="text-center py-12 text-gray-500 dark:text-gray-400">
|
||||||
|
<span class="i-lucide-map-pin text-4xl mb-4 block" />
|
||||||
|
<p class="text-lg">{{ t('addresses.noAddresses') }}</p>
|
||||||
|
<UButton @click="showModal"
|
||||||
|
class="mt-4 px-4 py-2 bg-primary text-white rounded-lg hover:bg-blue-600 transition-colors">
|
||||||
|
{{ t('addresses.addFirst') }}
|
||||||
|
</UButton>
|
||||||
|
</div>
|
||||||
|
<form @submit.prevent="saveAddress" class="space-y-4 mt-4">
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-black dark:text-white mb-1">Street *</label>
|
||||||
|
<UInput v-model="formData.street" placeholder="Enter street" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-black dark:text-white mb-1">Zip Code *</label>
|
||||||
|
<UInput v-model="formData.zipCode" placeholder="Enter zip code" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-black dark:text-white mb-1">City *</label>
|
||||||
|
<UInput v-model="formData.city" placeholder="Enter city" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-black dark:text-white mb-1">Country *</label>
|
||||||
|
<UInput v-model="formData.country" placeholder="Enter country" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex justify-end gap-3 pt-4 border-t border-gray-200 dark:border-gray-700">
|
||||||
|
<UButton color="neutral" variant="outline" @click="closeModal">Cancel</UButton>
|
||||||
|
<UButton type="submit" color="primary">Save</UButton>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, computed, watch } from 'vue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
import { useAddressStore, type Address, type AddressFormData } from '@/stores/address'
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
const addressStore = useAddressStore()
|
||||||
|
const { t } = useI18n()
|
||||||
|
|
||||||
|
const showModal = ref(false)
|
||||||
|
const isEditing = ref(false)
|
||||||
|
const editingAddressId = ref<number | null>(null)
|
||||||
|
|
||||||
|
const formData = ref<AddressFormData>({
|
||||||
|
street: '',
|
||||||
|
zipCode: '',
|
||||||
|
city: '',
|
||||||
|
country: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
const formErrors = ref<Record<string, string>>({})
|
||||||
|
const searchQuery = ref('')
|
||||||
|
|
||||||
|
let searchTimeout: ReturnType<typeof setTimeout> | null = null
|
||||||
|
|
||||||
|
watch(searchQuery, (newValue) => {
|
||||||
|
if (searchTimeout) clearTimeout(searchTimeout)
|
||||||
|
searchTimeout = setTimeout(() => {
|
||||||
|
addressStore.setSearchQuery(newValue)
|
||||||
|
}, 300)
|
||||||
|
})
|
||||||
|
|
||||||
|
const paginatedAddresses = computed(() => addressStore.paginatedAddresses)
|
||||||
|
|
||||||
|
function resetForm() {
|
||||||
|
formData.value = {
|
||||||
|
street: '',
|
||||||
|
zipCode: '',
|
||||||
|
city: '',
|
||||||
|
country: ''
|
||||||
|
}
|
||||||
|
formErrors.value = {}
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeModal() {
|
||||||
|
showModal.value = false
|
||||||
|
resetForm()
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateForm(): boolean {
|
||||||
|
formErrors.value = {}
|
||||||
|
|
||||||
|
if (!formData.value.street.trim()) {
|
||||||
|
formErrors.value.street = 'Street is required'
|
||||||
|
}
|
||||||
|
if (!formData.value.zipCode.trim()) {
|
||||||
|
formErrors.value.zipCode = 'Zip Code is required'
|
||||||
|
}
|
||||||
|
if (!formData.value.city.trim()) {
|
||||||
|
formErrors.value.city = 'City is required'
|
||||||
|
}
|
||||||
|
if (!formData.value.country.trim()) {
|
||||||
|
formErrors.value.country = 'Country is required'
|
||||||
|
}
|
||||||
|
|
||||||
|
return Object.keys(formErrors.value).length === 0
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveAddress() {
|
||||||
|
if (!validateForm()) return
|
||||||
|
|
||||||
|
if (isEditing.value && editingAddressId.value) {
|
||||||
|
(editingAddressId.value, formData.value)
|
||||||
|
} else {
|
||||||
|
addressStore.addAddress(formData.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
closeModal()
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
164
bo/src/components/customer/PageProductCardFull.vue
Normal file
164
bo/src/components/customer/PageProductCardFull.vue
Normal file
@@ -0,0 +1,164 @@
|
|||||||
|
<template>
|
||||||
|
<div class="container mt-14">
|
||||||
|
<div class="flex justify-between gap-8 mb-6">
|
||||||
|
<div class="flex-1">
|
||||||
|
<div class="bg-gray-100 dark:bg-gray-800 rounded-lg p-8 flex items-center justify-center min-h-[300px]">
|
||||||
|
<img :src="selectedColor?.image || productData.image" :alt="productData.name"
|
||||||
|
class="max-w-full h-auto object-contain" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex-1 flex flex-col gap-4">
|
||||||
|
<h1 class="text-2xl font-bold text-gray-900 dark:text-white">
|
||||||
|
{{ productData.name }}
|
||||||
|
</h1>
|
||||||
|
<p class="text-gray-600 dark:text-gray-300">
|
||||||
|
{{ productData.description }}
|
||||||
|
</p>
|
||||||
|
<div class="text-3xl font-bold text-(--accent-blue-light) dark:text-(--accent-blue-dark)">
|
||||||
|
{{ productData.price }}
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col">
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<span class="text-gray-500 dark:text-gray-400">Dimensions:</span>
|
||||||
|
<p class="font-medium dark:text-white">{{ productData.dimensions }}</p>
|
||||||
|
</div>
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<span class="text-gray-500 dark:text-gray-400">Seat Height:</span>
|
||||||
|
<p class="font-medium dark:text-white">{{ productData.seatHeight }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between items-end mb-8">
|
||||||
|
<div class="flex flex-col gap-3">
|
||||||
|
<span class="text-sm text-(--accent-blue-light) dark:text-(--accent-blue-dark) ">Colors:</span>
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<button v-for="color in productData.colors" :key="color.id" @click="selectedColor = color"
|
||||||
|
class="w-10 h-10 border-2 transition-all" :class="selectedColor?.id === color.id
|
||||||
|
? 'border-(--accent-blue-light) ring-2 ring-blue-600 ring-offset-2'
|
||||||
|
: 'border-gray-300 dark:border-gray-600 hover:border-gray-400'"
|
||||||
|
:style="{ backgroundColor: color.hex }" :title="color.name" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex gap-5 items-end">
|
||||||
|
<UInputNumber v-model="value" />
|
||||||
|
<UButton color="primary" class="px-14! bg-(--accent-blue-light) dark:bg-(--accent-blue-dark) text-white">
|
||||||
|
Add to Cart
|
||||||
|
</UButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<ProductCustomization />
|
||||||
|
<hr class="border-t border-(--border-light) dark:border-(--border-dark) mb-8" />
|
||||||
|
<div class="mb-6 w-[55%]">
|
||||||
|
<div class="flex justify-between items-center gap-10 mb-8">
|
||||||
|
<UButton v-for="tab in tabs" :key="tab.id" @click="activeTab = tab.id" :class="[
|
||||||
|
'px-15 py-2 cursor-pointer',
|
||||||
|
activeTab === tab.id
|
||||||
|
? 'bg-blue-600 hover:text-black hover:dark:text-white text-white'
|
||||||
|
: 'text-gray-700 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-700'
|
||||||
|
]" variant="ghost">
|
||||||
|
{{ tab.label }}
|
||||||
|
</UButton>
|
||||||
|
</div>
|
||||||
|
<div class="py-5 px-3 bg-(--second-light) dark:bg-(--main-dark) rounded-md">
|
||||||
|
<p class="dark:text-white whitespace-pre-line">
|
||||||
|
{{ activeTabContent }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<hr class="border-t border-(--border-light) dark:border-(--border-dark) mb-8" />
|
||||||
|
<ProductVariants />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, computed } from 'vue'
|
||||||
|
import ProductCustomization from './components/ProductCustomization.vue'
|
||||||
|
import ProductVariants from './components/ProductVariants.vue'
|
||||||
|
|
||||||
|
interface Color {
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
hex: string
|
||||||
|
image: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ProductData {
|
||||||
|
name: string
|
||||||
|
description: string
|
||||||
|
price: string
|
||||||
|
dimensions: string
|
||||||
|
seatHeight: string
|
||||||
|
image: string
|
||||||
|
colors: Color[]
|
||||||
|
descriptionText: string
|
||||||
|
howToUseText: string
|
||||||
|
productDetailsText: string
|
||||||
|
documentsText: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const activeTab = ref('description')
|
||||||
|
const value = ref(5)
|
||||||
|
const selectedColor = ref<Color | null>(null)
|
||||||
|
|
||||||
|
const productData: ProductData = {
|
||||||
|
name: 'Larger Corner Sofa',
|
||||||
|
description: 'The large corner sofa is a comfortable seating solution for young children. It is upholstered with phthalate-free PVC-coated material intended for medical products, making it very easy to clean and disinfect. The product is available in a wide range of colours (13 colours), allowing it to fit into any interior.',
|
||||||
|
price: 'PLN 519.00 (VAT 23%)',
|
||||||
|
dimensions: '65 x 65 x 120 cm',
|
||||||
|
seatHeight: '45-55 cm',
|
||||||
|
image: '/placeholder-chair.jpg',
|
||||||
|
colors: [
|
||||||
|
{ id: 'black', name: 'Black', hex: '#1a1a1a', image: '/chair-black.jpg' },
|
||||||
|
{ id: 'gray', name: 'Gray', hex: '#6b7280', image: '/chair-gray.jpg' },
|
||||||
|
{ id: 'blue', name: 'Blue', hex: '#3b82f6', image: '/chair-blue.jpg' },
|
||||||
|
{ id: 'brown', name: 'Brown', hex: '#92400e', image: '/chair-brown.jpg' },
|
||||||
|
],
|
||||||
|
descriptionText: 'The large corner sofa is a comfortable seating solution for young children. It is upholstered with phthalate-free PVC-coated material intended for medical products, making it very easy to clean and disinfect. The product is available in a wide range of colours (13 colours), allowing it to fit into any interior',
|
||||||
|
howToUseText: '1. Adjust the seat height using the lever under the seat.\n2. Set the lumbar support to your preferred position.\n3. Adjust the armrests for optimal arm support.\n4. Use the recline tension knob to adjust the backrest resistance.\n5. Lock the recline position when needed.',
|
||||||
|
productDetailsText: '• Material: Mesh, Foam, Plastic\n• Max Load: 150 kg\n• Weight: 18 kg\n• Warranty: 2 years\n• Certifications: BIFMA, EN 1335',
|
||||||
|
documentsText: '• Assembly Instructions (PDF)\n• User Manual (PDF)\n• Warranty Terms (PDF)\n• Safety Certificate (PDF)',
|
||||||
|
}
|
||||||
|
|
||||||
|
const tabs = [
|
||||||
|
{ id: 'description', label: 'Description' },
|
||||||
|
{ id: 'howToUse', label: 'How to Use' },
|
||||||
|
{ id: 'productDetails', label: 'Product Details' },
|
||||||
|
{ id: 'documents', label: 'Documents' },
|
||||||
|
]
|
||||||
|
|
||||||
|
const userActions = [
|
||||||
|
'View detailed product information',
|
||||||
|
'Browse product images and available colors',
|
||||||
|
'Check product dimensions and specifications',
|
||||||
|
'Select a product variant',
|
||||||
|
'Select quantity',
|
||||||
|
'Add the product to the cart',
|
||||||
|
'Navigate between product description, usage instructions, and product details',
|
||||||
|
]
|
||||||
|
|
||||||
|
const activeTabContent = computed(() => {
|
||||||
|
switch (activeTab.value) {
|
||||||
|
case 'description':
|
||||||
|
return productData.descriptionText
|
||||||
|
case 'howToUse':
|
||||||
|
return productData.howToUseText
|
||||||
|
case 'productDetails':
|
||||||
|
return productData.productDetailsText
|
||||||
|
case 'documents':
|
||||||
|
return productData.documentsText
|
||||||
|
default:
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (productData.colors.length > 0) {
|
||||||
|
selectedColor.value = productData.colors[0] as Color
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.product-card-full {
|
||||||
|
font-family: inherit;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
<template>
|
||||||
|
<div class="container flex flex-col gap-8">
|
||||||
|
<div class="space-y-1 dark:text-white text-black">
|
||||||
|
<p class="text-[24px] font-bold">Product customization</p>
|
||||||
|
<p class="text-[15px]">Don't forget to save your customization to be able to add to cart</p>
|
||||||
|
</div>
|
||||||
|
<div class="grid grid-cols-3 gap-10">
|
||||||
|
<UInput label="Podaj kolor kanapy narożnej" placeholder="Podaj kolor kanapy narożnej" class="dark:text-white text-black"/>
|
||||||
|
<UInput label="Podaj kolor fotela" placeholder="Podaj kolor fotela" class="dark:text-white text-black"/>
|
||||||
|
<UInput label="Podaj kolor kwadratu" placeholder="Podaj kolor kwadratu" class="dark:text-white text-black"/>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-end items-end mb-8">
|
||||||
|
<UButton class="px-10! bg-(--accent-blue-light) dark:bg-(--accent-blue-dark) text-white">Save</UButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
57
bo/src/components/customer/components/ProductVariants.vue
Normal file
57
bo/src/components/customer/components/ProductVariants.vue
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
<template>
|
||||||
|
<div class="container flex flex-col gap-8">
|
||||||
|
<p class="text-[24px] font-bold dark:text-white text-black">Product Variants:</p>
|
||||||
|
<div class="flex flex-col gap-4">
|
||||||
|
<div v-for="(variant, index) in variants" :key="index" class="flex gap-10">
|
||||||
|
<div
|
||||||
|
class="flex items-center gap-15 border border-(--border-light) dark:border-(--border-dark) p-5 rounded-md hover:bg-gray-50 hover:dark:bg-gray-700 bg-(--second-light) dark:bg-(--second-dark) dark:text-white text-black">
|
||||||
|
<img :src="variant.image" :alt="variant.image" class="w-16 h-16 object-cover" />
|
||||||
|
<p class="">{{ variant.name }}</p>
|
||||||
|
<p class="">{{ variant.productNumber }}</p>
|
||||||
|
<p class="">{{ variant.value }}</p>
|
||||||
|
<p class="">{{ variant.price }}</p>
|
||||||
|
<p class="">{{ variant.quantity }}</p>
|
||||||
|
<button class="bg-blue-500 hover:bg-blue-600 text-white px-4 py-2 rounded"
|
||||||
|
@click="addToCart(variant)">
|
||||||
|
Add to Cart
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from 'vue'
|
||||||
|
|
||||||
|
const variants = ref([
|
||||||
|
{
|
||||||
|
image: 'img',
|
||||||
|
name: 'Duży fotelik narożny ',
|
||||||
|
productNumber: 'NC209/7000',
|
||||||
|
value: '20,000',
|
||||||
|
price: '519,00 zł',
|
||||||
|
quantity: 10
|
||||||
|
},
|
||||||
|
{
|
||||||
|
image: 'img',
|
||||||
|
name: 'Duży fotelik narożny ',
|
||||||
|
productNumber: 'NC209/7000',
|
||||||
|
value: '20,000',
|
||||||
|
price: '519,00 zł',
|
||||||
|
quantity: 5
|
||||||
|
},
|
||||||
|
{
|
||||||
|
image: 'img',
|
||||||
|
name: 'Duży fotelik narożny ',
|
||||||
|
productNumber: 'NC209/7000',
|
||||||
|
value: '20,000',
|
||||||
|
price: '519,00 zł',
|
||||||
|
quantity: 8
|
||||||
|
}
|
||||||
|
])
|
||||||
|
|
||||||
|
const addToCart = (variant: any) => {
|
||||||
|
console.log('Added to cart:', variant)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -5,8 +5,6 @@ import { getSettings } from './settings'
|
|||||||
import { useAuthStore } from '@/stores/auth'
|
import { useAuthStore } from '@/stores/auth'
|
||||||
import Default from '@/layouts/default.vue'
|
import Default from '@/layouts/default.vue'
|
||||||
|
|
||||||
// Helper: read the non-HTTPOnly is_authenticated cookie set by the backend.
|
|
||||||
// The backend sets it to "1" on login and removes it on logout.
|
|
||||||
function isAuthenticated(): boolean {
|
function isAuthenticated(): boolean {
|
||||||
if (typeof document === 'undefined') return false
|
if (typeof document === 'undefined') return false
|
||||||
return document.cookie.split('; ').some((c) => c === 'is_authenticated=1')
|
return document.cookie.split('; ').some((c) => c === 'is_authenticated=1')
|
||||||
@@ -31,8 +29,10 @@ const router = createRouter({
|
|||||||
component: Default,
|
component: Default,
|
||||||
children: [
|
children: [
|
||||||
{ path: '', component: () => import('../views/RepoChartView.vue'), name: 'home' },
|
{ path: '', component: () => import('../views/RepoChartView.vue'), name: 'home' },
|
||||||
{ path: 'products', component: () => import('../views/customer/ProductsView.vue'), name: 'products' },
|
{ path: 'products', component: () => import('../components/admin/ProductsView.vue'), name: 'products' },
|
||||||
{ path: 'products-datail/', component: () => import('../views/customer/ProductDetailView.vue'), name: 'product-detail' },
|
{ path: 'products-datail/', component: () => import('../components/admin/ProductDetailView.vue'), name: 'product-detail' },
|
||||||
|
{ path: 'product-card-full/', component: () => import('../components/customer/PageProductCardFull.vue'), name: 'product-card-full' },
|
||||||
|
{ path: 'addresses', component: () => import('../components/customer/PageAddresses.vue'), name: 'addresses' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -51,34 +51,28 @@ const router = createRouter({
|
|||||||
],
|
],
|
||||||
})
|
})
|
||||||
|
|
||||||
// Navigation guard: language handling + auth protection
|
|
||||||
router.beforeEach((to, from, next) => {
|
router.beforeEach((to, from, next) => {
|
||||||
const locale = to.params.locale as string
|
const locale = to.params.locale as string
|
||||||
const localeLang = langs.find((x) => x.iso_code == locale)
|
const localeLang = langs.find((x) => x.iso_code == locale)
|
||||||
|
|
||||||
// Check if the locale is valid
|
|
||||||
if (locale && langs.length > 0) {
|
if (locale && langs.length > 0) {
|
||||||
const authStore = useAuthStore()
|
const authStore = useAuthStore()
|
||||||
console.log(authStore.isAuthenticated,to, from)
|
console.log(authStore.isAuthenticated, to, from)
|
||||||
// if()
|
// if()
|
||||||
const validLocale = langs.find((l) => l.lang_code === locale)
|
const validLocale = langs.find((l) => l.lang_code === locale)
|
||||||
|
|
||||||
if (validLocale) {
|
if (validLocale) {
|
||||||
currentLang.value = localeLang
|
currentLang.value = localeLang
|
||||||
|
|
||||||
// Auth guard: if the route does NOT have meta.guest = true, require authentication
|
|
||||||
if (!to.meta?.guest && !isAuthenticated()) {
|
if (!to.meta?.guest && !isAuthenticated()) {
|
||||||
return next({ name: 'login', params: { locale } })
|
return next({ name: 'login', params: { locale } })
|
||||||
}
|
}
|
||||||
|
|
||||||
return next()
|
return next()
|
||||||
} else if (locale) {
|
} else if (locale) {
|
||||||
// Invalid locale - redirect to default language
|
|
||||||
return next(`/${currentLang.value?.iso_code}${to.path.replace(`/${locale}`, '') || '/'}`)
|
return next(`/${currentLang.value?.iso_code}${to.path.replace(`/${locale}`, '') || '/'}`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// No locale in URL - redirect to default language
|
|
||||||
if (!locale && to.path !== '/') {
|
if (!locale && to.path !== '/') {
|
||||||
return next(`/${currentLang.value?.iso_code}${to.path}`)
|
return next(`/${currentLang.value?.iso_code}${to.path}`)
|
||||||
}
|
}
|
||||||
|
|||||||
136
bo/src/stores/address.ts
Normal file
136
bo/src/stores/address.ts
Normal file
@@ -0,0 +1,136 @@
|
|||||||
|
import { defineStore } from 'pinia'
|
||||||
|
import { ref, computed } from 'vue'
|
||||||
|
|
||||||
|
export interface AddressFormData {
|
||||||
|
street: string
|
||||||
|
zipCode: string
|
||||||
|
city: string
|
||||||
|
country: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Address {
|
||||||
|
id: number
|
||||||
|
street: string
|
||||||
|
zipCode: string
|
||||||
|
city: string
|
||||||
|
country: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useAddressStore = defineStore('address', () => {
|
||||||
|
const addresses = ref<Address[]>([])
|
||||||
|
const loading = ref(false)
|
||||||
|
const error = ref<string | null>(null)
|
||||||
|
|
||||||
|
const currentPage = ref(1)
|
||||||
|
const pageSize = 10
|
||||||
|
const totalItems = computed(() => filteredAddresses.value.length)
|
||||||
|
const totalPages = computed(() => Math.ceil(totalItems.value / pageSize))
|
||||||
|
|
||||||
|
const searchQuery = ref('')
|
||||||
|
|
||||||
|
function initMockData() {
|
||||||
|
addresses.value = [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
street: 'Main Street 123',
|
||||||
|
zipCode: '10-001',
|
||||||
|
city: 'New York',
|
||||||
|
country: 'United States'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
street: 'Oak Avenue 123',
|
||||||
|
zipCode: '90-001',
|
||||||
|
city: 'Los Angeles',
|
||||||
|
country: 'United States'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
street: 'Pine Road 123 ',
|
||||||
|
zipCode: '60-601',
|
||||||
|
city: 'Chicago',
|
||||||
|
country: 'United States'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
const filteredAddresses = computed(() => {
|
||||||
|
if (!searchQuery.value) {
|
||||||
|
return addresses.value
|
||||||
|
}
|
||||||
|
|
||||||
|
const query = searchQuery.value.toLowerCase()
|
||||||
|
|
||||||
|
return addresses.value.filter(addr =>
|
||||||
|
addr.street.toLowerCase().includes(query) ||
|
||||||
|
addr.city.toLowerCase().includes(query) ||
|
||||||
|
addr.country.toLowerCase().includes(query) ||
|
||||||
|
addr.zipCode.toLowerCase().includes(query)
|
||||||
|
)
|
||||||
|
})
|
||||||
|
const paginatedAddresses = computed(() => {
|
||||||
|
const start = (currentPage.value - 1) * pageSize
|
||||||
|
const end = start + pageSize
|
||||||
|
return filteredAddresses.value.slice(start, end)
|
||||||
|
})
|
||||||
|
|
||||||
|
function getAddressById(id: number): Address | undefined {
|
||||||
|
return addresses.value.find(addr => addr.id === id)
|
||||||
|
}
|
||||||
|
|
||||||
|
function addAddress(formData: AddressFormData): Address {
|
||||||
|
const now = new Date().toISOString()
|
||||||
|
|
||||||
|
const newAddress: Address = {
|
||||||
|
id: Date.now(),
|
||||||
|
...formData,
|
||||||
|
}
|
||||||
|
|
||||||
|
addresses.value.push(newAddress)
|
||||||
|
|
||||||
|
return newAddress
|
||||||
|
}
|
||||||
|
|
||||||
|
function deleteAddress(id: number): boolean {
|
||||||
|
const index = addresses.value.findIndex(addr => addr.id === id)
|
||||||
|
if (index === -1) return false
|
||||||
|
|
||||||
|
addresses.value.splice(index, 1)
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
function setPage(page: number) {
|
||||||
|
currentPage.value = page
|
||||||
|
}
|
||||||
|
|
||||||
|
function setSearchQuery(query: string) {
|
||||||
|
searchQuery.value = query
|
||||||
|
currentPage.value = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetPagination() {
|
||||||
|
currentPage.value = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
initMockData()
|
||||||
|
|
||||||
|
return {
|
||||||
|
addresses,
|
||||||
|
loading,
|
||||||
|
error,
|
||||||
|
currentPage,
|
||||||
|
pageSize,
|
||||||
|
totalItems,
|
||||||
|
totalPages,
|
||||||
|
searchQuery,
|
||||||
|
filteredAddresses,
|
||||||
|
paginatedAddresses,
|
||||||
|
getAddressById,
|
||||||
|
addAddress,
|
||||||
|
deleteAddress,
|
||||||
|
setPage,
|
||||||
|
setSearchQuery,
|
||||||
|
resetPagination
|
||||||
|
}
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user