fix: added page PageProductCardFull and Addresses

This commit is contained in:
2026-03-17 14:12:02 +01:00
parent 7388d0f828
commit 789d59b0c9
11 changed files with 556 additions and 12 deletions

View File

@@ -22,6 +22,12 @@ const authStore = useAuthStore()
<RouterLink :to="{ name: 'product-detail' }">
product detail
</RouterLink>
<RouterLink :to="{ name: 'product-card-full' }">
ProductCardFull
</RouterLink>
<RouterLink :to="{ name: 'addresses' }">
Addresses
</RouterLink>
<div class="flex items-center gap-2">
<!-- Language Switcher -->
<LangSwitch />

View File

@@ -0,0 +1,222 @@
<template>
<div class="container my-10 ">
<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="iso_code">
<template #default="{ modelValue }">
<div class="flex items-center gap-2">
<span class="text-md">{{availableLangs.find(x => x.iso_code == modelValue)?.flag}}</span>
<span class="font-medium dark:text-white text-black">{{availableLangs.find(x => x.iso_code ==
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>
<!-- Loading Overlay -->
<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 class="flex items-start gap-30">
<p class="p-80 bg-(--second-light)">img</p>
<div class="flex flex-col gap-2">
<p class="text-[25px] font-bold text-black dark:text-white">
{{ productStore.productDescription.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 }}
</p>
</div>
</div>
</div>
</div>
<div 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>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue'
import { useProductStore } from '@/stores/product'
import { useEditable } from '@/composable/useConteditable'
import { langs } from '@/router/langs'
import type { Language } from '@/types'
const activeTab = ref('description')
const productStore = useProductStore()
const translating = ref(false)
// const availableLangs = computed(() => {
// return langs.filter((l: Language) => ['cs', 'pl', 'der'].includes(l.iso_code))
// })
const isEditing = ref(false)
const availableLangs = computed(() => langs)
const selectedLanguage = ref('pl')
const currentLangId = ref(2)
const fetchForLanguage = async (langCode: string) => {
const lang = langs.find((l: Language) => l.iso_code === langCode)
if (lang) {
await productStore.getProductDescription(lang.id)
currentLangId.value = lang.id
}
}
const translateToSelectedLanguage = async () => {
const targetLang = langs.find((l: Language) => l.iso_code === selectedLanguage.value)
if (targetLang && currentLangId.value) {
translating.value = true
try {
await productStore.translateProductDescription(currentLangId.value, targetLang.id)
currentLangId.value = targetLang.id
} finally {
translating.value = false
}
}
}
await fetchForLanguage(selectedLanguage.value)
const descriptionRef = ref<HTMLElement | null>(null)
const usageRef = ref<HTMLElement | null>(null)
const descriptionEdit = useEditable(descriptionRef)
const usageEdit = useEditable(usageRef)
const originalDescription = ref('')
const originalUsage = ref('')
const saveDescription = async () => {
descriptionEdit.disableEdit()
await productStore.saveProductDescription()
}
const cancelDescriptionEdit = () => {
if (descriptionRef.value) {
descriptionRef.value.innerHTML = originalDescription.value
}
descriptionEdit.disableEdit()
}
const enableDescriptionEdit = () => {
if (descriptionRef.value) {
originalDescription.value = descriptionRef.value.innerHTML
}
descriptionEdit.enableEdit()
}
const enableEdit = () => {
if (usageRef.value) {
originalUsage.value = usageRef.value.innerHTML
}
isEditing.value = true
usageEdit.enableEdit()
}
const saveText = () => {
usageEdit.disableEdit()
isEditing.value = false
productStore.saveProductDescription()
}
const cancelEdit = () => {
if (usageRef.value) {
usageRef.value.innerHTML = originalUsage.value
}
usageEdit.disableEdit()
isEditing.value = false
}
</script>
<style>
.images {
display: flex;
align-items: center;
gap: 70px;
margin: 20px 0 20px 0;
}
</style>

View File

@@ -0,0 +1,257 @@
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { useAuthStore } from '@/stores/auth'
import { useProductStore, type Product } from '@/stores/product'
import { useI18n } from 'vue-i18n'
import type { TableColumn } from '@nuxt/ui'
import { h } from 'vue'
const router = useRouter()
const authStore = useAuthStore()
const productStore = useProductStore()
const { t } = useI18n()
const searchName = ref('')
const searchCode = ref('')
const priceFromFilter = ref<number | null>(null)
const priceToFilter = ref<number | null>(null)
// Pagination
const page = ref(1)
const pageSize = 5
// Fetch products on mount
onMounted(() => {
productStore.getProductDescription()
})
// Filtered products
// const filteredProducts = computed(() => {
// console.log(productStore.products);
// return productStore.products.filter(product => {
// const matchesName = product.name.toLowerCase().includes(searchName.value.toLowerCase())
// const matchesCode = product.code.toLowerCase().includes(searchCode.value.toLowerCase())
// const matchesPriceFrom = priceFromFilter.value === null || product.priceFrom >= priceFromFilter.value
// const matchesPriceTo = priceToFilter.value === null || product.priceTo <= priceToFilter.value
// return matchesName && matchesCode && matchesPriceFrom && matchesPriceTo
// })
// })
// const totalItems = computed(() => filteredProducts.value.length)
// const paginatedProducts = computed(() => {
// const start = (page.value - 1) * pageSize
// const end = start + pageSize
// return filteredProducts.value.slice(start, end)
// })
// Reset page when filters change
function resetPage() {
page.value = 1
}
// Navigate to product detail
function goToProduct(product: Product) {
router.push({ name: 'product-detail', params: { id: product.id } })
}
// Table columns
const columns = computed<TableColumn<Product>[]>(() => [
{
accessorKey: 'image',
header: () => h('div', { class: 'text-center' }, t('products.image')),
cell: ({ row }) => h('img', {
src: row.getValue('image'),
alt: 'Product',
class: 'w-12 h-12 object-cover rounded'
})
},
{
accessorKey: 'name',
header: t('products.product_name'),
cell: ({ row }) => {
const product = row.original
return h('button', {
class: 'text-primary hover:underline font-medium text-left',
onClick: (e: Event) => { e.stopPropagation(); goToProduct(product) }
}, product.name)
}
},
{
accessorKey: 'code',
header: t('products.product_code'),
},
{
accessorKey: 'description',
header: t('products.description'),
cell: ({ row }) => {
const desc = row.getValue('description') as string
return h('span', { class: 'text-sm text-gray-500 dark:text-gray-400' }, desc?.substring(0, 50) + (desc && desc.length > 50 ? '...' : ''))
}
},
{
accessorKey: 'inStock',
header: t('products.in_stock'),
cell: ({ row }) => {
const inStock = row.getValue('inStock')
return h('span', {
class: inStock ? 'text-green-600 font-medium' : 'text-red-600 font-medium'
}, inStock ? t('products.yes') : t('products.no'))
}
},
{
accessorKey: 'price',
header: t('products.price'),
cell: ({ row }) => {
const priceFromVal = row.original.priceFrom
const priceToVal = row.original.priceTo
return `${priceFromVal} - ${priceToVal}`
}
},
{
accessorKey: 'count',
header: t('products.count'),
},
{
id: 'actions',
header: '',
cell: ({ row }) => {
const product = row.original
return h('div', { class: 'flex gap-2' }, [
h('button', {
class: 'px-3 py-1.5 text-sm font-medium bg-primary text-white rounded-lg hover:bg-blue-600 transition-colors',
onClick: (e: Event) => { e.stopPropagation(); addToCart(product) }
}, t('products.add_to_cart')),
h('button', {
class: 'px-3 py-1.5 text-sm font-medium bg-gray-200 dark:bg-gray-700 text-black dark:text-white rounded-lg hover:bg-gray-300 dark:hover:bg-gray-600 transition-colors',
onClick: (e: Event) => { e.stopPropagation(); incrementCount(product) }
}, '+')
])
}
}
])
// Actions
function addToCart(product: Product) {
console.log('Add to cart:', product)
}
function incrementCount(product: Product) {
product.count++
}
function clearFilters() {
searchName.value = ''
searchCode.value = ''
priceFromFilter.value = null
priceToFilter.value = null
resetPage()
}
</script>
<template>
<div class="container">
<div class="p-6 bg-white dark:bg-(--black) min-h-screen font-sans">
<div >
<!-- v-html="productStore.products" -->
</div>
<h1 class="text-2xl font-bold mb-6 text-black dark:text-white">{{ t('products.title') }}</h1>
<div v-if="!authStore.isAuthenticated" class="mb-4 p-3 bg-yellow-100 text-yellow-700 rounded">
{{ t('products.login_to_view') }}
</div>
<!-- Loading State -->
<div v-if="productStore.loading" class="mb-4 p-3 bg-blue-100 text-blue-700 rounded">
{{ t('products.loading') }}...
</div>
<!-- Error State -->
<div v-if="productStore.error" class="mb-4 p-3 bg-red-100 text-red-700 rounded">
{{ productStore.error }}
</div>
<div v-if="authStore.isAuthenticated && !productStore.loading && !productStore.error" class="space-y-4">
<!-- Filter Block -->
<div class="flex flex-wrap gap-4 mb-4 p-4 border border-(--border-light) dark:border-(--border-dark) rounded bg-gray-50 dark:bg-gray-800">
<div class="flex flex-col min-w-[180px]">
<label class="mb-1 text-sm font-medium text-black dark:text-white">{{ t('products.search_by_name') }}</label>
<UInput
v-model="searchName"
:placeholder="t('products.search_name_placeholder')"
@update:model-value="resetPage"
class="dark:text-white text-black"
/>
</div>
<div class="flex flex-col min-w-[180px]">
<label class="mb-1 text-sm font-medium text-black dark:text-white">{{ t('products.search_by_code') }}</label>
<UInput
v-model="searchCode"
:placeholder="t('products.search_code_placeholder')"
@update:model-value="resetPage"
class="dark:text-white text-black"
/>
</div>
<div class="flex flex-col min-w-[120px]">
<label class="mb-1 text-sm font-medium text-black dark:text-white">{{ t('products.price_from') }}</label>
<UInput
v-model="priceFromFilter"
type="number"
:placeholder="t('products.price_from')"
@update:model-value="resetPage"
class="dark:text-white text-black"
/>
</div>
<div class="flex flex-col min-w-[120px]">
<label class="mb-1 text-sm font-medium text-black dark:text-white">{{ t('products.price_to') }}</label>
<UInput
v-model="priceToFilter"
type="number"
:placeholder="t('products.price_to')"
@update:model-value="resetPage"
class="dark:text-white text-black"
/>
</div>
<div class="flex items-end">
<button
@click="clearFilters"
class="px-4 py-2 text-sm font-medium text-black dark:text-white bg-gray-200 dark:bg-gray-700 rounded-lg hover:bg-gray-300 dark:hover:bg-gray-600 transition-colors"
>
{{ t('products.clear_filters') }}
</button>
</div>
</div>
<!-- Products Table -->
<!-- <div class="border border-(--border-light) dark:border-(--border-dark) rounded overflow-hidden">
<UTable
:data="paginatedProducts"
:columns="columns"
class="dark:text-white! text-dark"
/>
</div> -->
<!-- Empty State -->
<!-- <div v-if="filteredProducts.length === 0" class="text-center py-10 text-gray-500 dark:text-gray-400">
{{ t('products.no_products') }}
</div> -->
<!-- Pagination -->
<!-- <div v-if="filteredProducts.length > 0" class="pt-4 flex justify-center items-center dark:text-white! text-dark">
<UPagination
v-model:page="page"
:page-count="pageSize"
:total="totalItems"
/>
</div> -->
<!-- Results count -->
<!-- <div v-if="filteredProducts.length > 0" class="text-sm text-gray-600 dark:text-gray-400 text-center">
{{ t('products.showing') }} {{ paginatedProducts.length }} {{ t('products.of') }} {{ totalItems }} {{ t('products.products') }}
</div> -->
</div>
</div>
</div>
</template>

View 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>

View 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>

View File

@@ -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>

View 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>