Redesign productDetail dialog
This commit is contained in:
@@ -2,23 +2,38 @@
|
||||
"data": [
|
||||
{
|
||||
"id": 0,
|
||||
"icon": "",
|
||||
"name": "All"
|
||||
},
|
||||
{
|
||||
"id": 1,
|
||||
"icon": "mdi-chip",
|
||||
"name": "Electronic"
|
||||
},
|
||||
{
|
||||
"id": 1,
|
||||
"id": 2,
|
||||
"icon": "mdi-soccer",
|
||||
"name": "Sports"
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"id": 3,
|
||||
"icon": "mdi-tshirt-crew",
|
||||
"name": "Clothes"
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"id": 4,
|
||||
"icon": "mdi-bookshelf",
|
||||
"name": "Books"
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"icon": "mdi-guitar-electric",
|
||||
"name": "Instruments"
|
||||
},
|
||||
{
|
||||
"id": 6,
|
||||
"icon": "mdi-teddy-bear",
|
||||
"name": "Toys"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -5,99 +5,116 @@
|
||||
"brand": "Lenovo",
|
||||
"name": "Thinkpad T14",
|
||||
"price": 799.99,
|
||||
"categoryId": 0,
|
||||
"categoryId": 1,
|
||||
"discount": 10,
|
||||
"rating": 4.6,
|
||||
"description": "Die stabile Arbeitsmaschine. Mit AMD Ryzen 7 89029U, 128 GB RAM und 8 TB M.2 SSD!",
|
||||
"imageUrl": "thinkpad-t14s.jpg"
|
||||
"images": ["thinkpad-t14s.jpg"]
|
||||
},
|
||||
{
|
||||
"id": 1,
|
||||
"brand": "Puma",
|
||||
"name": "Men's Shirt",
|
||||
"price": 14.99,
|
||||
"categoryId": 2,
|
||||
"categoryId": 3,
|
||||
"discount": 0,
|
||||
"rating": 3.8,
|
||||
"description": "Normale Passform, ideal für den sportlichen Alltag.",
|
||||
"imageUrl": "puma-t-shirt-men.avif"
|
||||
"images": ["puma-t-shirt-men.avif"]
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"brand": "Puma",
|
||||
"name": "Woman's Shirt",
|
||||
"price": 14.99,
|
||||
"categoryId": 2,
|
||||
"categoryId": 3,
|
||||
"discount": 0,
|
||||
"rating": 4.0,
|
||||
"description": "Normale Passform, ideal für den sportlichen Alltag.",
|
||||
"imageUrl": "puma-t-shirt-woman.avif"
|
||||
"images": ["puma-t-shirt-woman.avif"]
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"brand": "George Orwell",
|
||||
"name": "1984",
|
||||
"price": 9.99,
|
||||
"categoryId": 3,
|
||||
"categoryId": 4,
|
||||
"discount": 0,
|
||||
"rating": 4.9,
|
||||
"imageUrl": "1984.jpg",
|
||||
"description": "BIG BROTHER IS WATCHING YOU! George Orwells 1984 ist längst zu einer scheinbar nicht mehr erklärungsbedürftigen Metapher für totalitäre Verhältnisse geworden. Mit atemberaubender Unerbittlichkeit zeichnet der Autor das erschreckende Bild einer durch und..."
|
||||
"description": "BIG BROTHER IS WATCHING YOU! George Orwells 1984 ist längst zu einer scheinbar nicht mehr erklärungsbedürftigen Metapher für totalitäre Verhältnisse geworden. Mit atemberaubender Unerbittlichkeit zeichnet der Autor das erschreckende Bild einer durch und...",
|
||||
"images": ["1984.jpg"]
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"brand": "Johann W. Goethe",
|
||||
"name": "Faust",
|
||||
"price": 4.99,
|
||||
"categoryId": 3,
|
||||
"categoryId": 4,
|
||||
"discount": 0,
|
||||
"rating": 4.2,
|
||||
"description": "Goethe schrieb über 60 Jahre an seinem »Faust« und nannte »diese sehr ernsten Scherze« am Ende sein »Hauptgeschäft«: Dabei entstand eines der großartigsten und gleichzeitig komplexesten Werke der Weltliteratur. Den »Faust« gibt es bei Reclam in vielen Ausgaben, preisniedrig für Schüler, mit Kommentar für Studenten, bibliophil für Liebhaber – jetzt endlich auch eine Doppelausgabe der beiden klassischen Theatertexte in der Universal-Bibliothek: »Faust I« und »II« in einem Band.",
|
||||
"imageUrl": "goethe-faust.jpg"
|
||||
"images": ["goethe-faust.jpg"]
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"brand": "Theodor Sturm",
|
||||
"name": "Der Schimmelreiter",
|
||||
"price": 4.99,
|
||||
"categoryId": 3,
|
||||
"categoryId": 4,
|
||||
"discount": 0,
|
||||
"rating": 3.5,
|
||||
"description": "Hauke Haien ist ein genialer Außenseiter, der sich als junger Deichgraf einen Jugendtraum erfüllt: den Bau eines neuartigen Deiches, der den Wellen besser standhalten soll. Die Dorfbewohner sind skeptisch und sehen in ihm die Verkörperung einer uralten Sage: Wenn er auf seinem Schimmel über den Deich reitet, wird Hauke Haien zum dämonischen Reiter, der ihr Leben und ihre Gesetze aus dem Gleichgewicht bringt. Theodor Storms bekannteste Novelle ist ein Meisterwerk realistischer Erzählkunst, in dem es um den Widerstreit von Rationalität und Aberglaube, Fortschritt und Tradition geht.",
|
||||
"imageUrl": "der-schimmelreiter.jpg"
|
||||
"images": ["der-schimmelreiter.jpg"]
|
||||
},
|
||||
{
|
||||
"id": 6,
|
||||
"brand": "Aldous Huxley",
|
||||
"name": "Brave New World",
|
||||
"price": 7.99,
|
||||
"categoryId": 3,
|
||||
"categoryId": 4,
|
||||
"discount": 0,
|
||||
"rating": 4.4,
|
||||
"imageUrl": "brave-new-world.jpg",
|
||||
"description": "Brave New World beschreibt eine genormte Gesellschaft, in der Föten genetisch manipuliert und Menschen konditioniert werden. Ziel des Staates ist Zufriedenheit und Stabilität, und dies wird durch Gleichheit, Drogen und Propaganda erreicht. Gott und Religion..."
|
||||
"description": "Brave New World beschreibt eine genormte Gesellschaft, in der Föten genetisch manipuliert und Menschen konditioniert werden. Ziel des Staates ist Zufriedenheit und Stabilität, und dies wird durch Gleichheit, Drogen und Propaganda erreicht. Gott und Religion...",
|
||||
"images": ["brave-new-world.jpg"]
|
||||
},
|
||||
{
|
||||
"id": 7,
|
||||
"brand": "Ankermann",
|
||||
"name": "Gaming Work V3",
|
||||
"price": 1299.99,
|
||||
"categoryId": 0,
|
||||
"categoryId": 1,
|
||||
"discount": 0,
|
||||
"rating": 4.7,
|
||||
"description": "Der schnellste Desktop-PC auf dem Markt! Intel Core i9 19650, 128 GB RAM, Nvidia Geforce RTX 6090. Gepaart mit 8 TB SSD Speicher in einem schicken RGB Gehäuse.",
|
||||
"imageUrl": "ankerman-gaming-work-v3.jpg"
|
||||
"images": ["ankerman-gaming-work-v3.jpg"]
|
||||
},
|
||||
{
|
||||
"id": 8,
|
||||
"brand": "Adidas",
|
||||
"name": "UCL Pro 23/24",
|
||||
"price": 99.99,
|
||||
"categoryId": 1,
|
||||
"categoryId": 2,
|
||||
"discount": 0,
|
||||
"rating": 3.9,
|
||||
"description": "Der Adidas UCL Pro ist beim Kicken für Hobbyspieler, Amateure und Profis unsere Nummer Eins. Denn der offizielle Spielball der Champions League hat eine wunderbar weiche, aber dennoch sehr robuste und abriebfeste Oberfläche. Mehr Ballgefühl als mit dem UCL Pro kann man kaum haben. Seine Außenhaut gefällt uns noch einen Tick besser als die der Mitbewerber. Die einzelnen Panels aus Polyurethan sind thermisch verklebt. Dadurch ist der Ball nahtlos und hat eine perfekte Kugelform.",
|
||||
"imageUrl": "fussball-champions-league.jpg"
|
||||
"images": ["fussball-champions-league.jpg"]
|
||||
},
|
||||
{
|
||||
"id": 9,
|
||||
"brand": "Fender",
|
||||
"name": "Player II Jazz Bass RW 3TS",
|
||||
"price": 899.99,
|
||||
"categoryId": 5,
|
||||
"discount": 5,
|
||||
"rating": 4.8,
|
||||
"description": "The Fender Player II JazzBass RW 3TS 3-Color Sunburst relies on great features that stand out in terms of comfort, playability and look, as well as providing all the advantages for a wide variety of genres. Whether dynamic bass lines or lush rock riffs: the Fender Player II Jazz Bass is a versatile E-Bass that is not only perfect for practice or studio sessions, but also functions as an assertive bass carpet in band contexts and live mixes. The robust alder body has a classic 3-color Sunburst-Finish finish and shines with easy handling and a comfortable grip - whether sitting or standing. The maple neck with a modern 'C' neck profile is fitted with a rosewood fingerboard, which is equipped with 20 medium jumbo frets and a fingerboard radius of 9.5', making it easy to use a wide variety of bass techniques and enabling quick fretting and precise riffs to be tackled cleanly.",
|
||||
"images": [
|
||||
"fender-player-ii-jazz-bass-rw-3ts-1.jpg",
|
||||
"fender-player-ii-jazz-bass-rw-3ts-2.jpg",
|
||||
"fender-player-ii-jazz-bass-rw-3ts-3.jpg",
|
||||
"fender-player-ii-jazz-bass-rw-3ts-4.jpg",
|
||||
"fender-player-ii-jazz-bass-rw-3ts-5.jpg"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
BIN
software/backend/images/fender-player-ii-jazz-bass-rw-3ts-1.jpg
Normal file
BIN
software/backend/images/fender-player-ii-jazz-bass-rw-3ts-1.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 40 KiB |
BIN
software/backend/images/fender-player-ii-jazz-bass-rw-3ts-2.jpg
Normal file
BIN
software/backend/images/fender-player-ii-jazz-bass-rw-3ts-2.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 36 KiB |
BIN
software/backend/images/fender-player-ii-jazz-bass-rw-3ts-3.jpg
Normal file
BIN
software/backend/images/fender-player-ii-jazz-bass-rw-3ts-3.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 27 KiB |
BIN
software/backend/images/fender-player-ii-jazz-bass-rw-3ts-4.jpg
Normal file
BIN
software/backend/images/fender-player-ii-jazz-bass-rw-3ts-4.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 27 KiB |
BIN
software/backend/images/fender-player-ii-jazz-bass-rw-3ts-5.jpg
Normal file
BIN
software/backend/images/fender-player-ii-jazz-bass-rw-3ts-5.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 23 KiB |
@@ -1,4 +1,4 @@
|
||||
import { Table, Column, Model, ForeignKey, BelongsTo, BelongsToMany, HasMany } from 'sequelize-typescript';
|
||||
import { Table, Column, Model, ForeignKey, BelongsTo, BelongsToMany, HasMany, DataType } from 'sequelize-typescript';
|
||||
import { Category } from './category.model';
|
||||
import { OrderItem } from './orderItem.model';
|
||||
|
||||
@@ -23,8 +23,17 @@ export class Product extends Model {
|
||||
@Column
|
||||
rating: number
|
||||
|
||||
@Column
|
||||
imageUrl: string
|
||||
@Column({
|
||||
type: DataType.STRING,
|
||||
get(): Array<string> {
|
||||
return this.getDataValue('images').split(';')
|
||||
},
|
||||
set(value: Array<string>) {
|
||||
console.log(value)
|
||||
this.setDataValue('images', value.join(';'))
|
||||
}
|
||||
})
|
||||
images: Array<string>
|
||||
|
||||
@Column
|
||||
description: string
|
||||
|
||||
@@ -5,7 +5,14 @@ const showDialog: ModelRef<boolean> = defineModel()
|
||||
|
||||
defineProps({
|
||||
title: String,
|
||||
subtitle: String,
|
||||
icon: {
|
||||
type: String,
|
||||
default: "mdi-cog"
|
||||
},
|
||||
subtitle: {
|
||||
type: String,
|
||||
default: ""
|
||||
},
|
||||
imageUrl: {
|
||||
type: String,
|
||||
default: ""
|
||||
@@ -14,21 +21,9 @@ defineProps({
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-dialog max-width="1000" v-model="showDialog">
|
||||
<v-card :title="title" :subtitle="subtitle" >
|
||||
<v-container>
|
||||
<v-row>
|
||||
<v-col>
|
||||
<v-img v-if="imageUrl != ''" :src="imageUrl" max-height="600" />
|
||||
</v-col>
|
||||
|
||||
<v-col>
|
||||
<v-card-text>
|
||||
<slot name="content"></slot>
|
||||
</v-card-text>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-container>
|
||||
<v-dialog max-width="1200" v-model="showDialog">
|
||||
<v-card :title="title" :subtitle="subtitle" :prepend-icon="icon" >
|
||||
<slot name="content"></slot>
|
||||
|
||||
<v-card-actions>
|
||||
<slot name="actions"></slot>
|
||||
|
||||
@@ -15,6 +15,7 @@ const navRail = defineModel("navRail", { type: Boolean })
|
||||
<div v-if="!navRail">{{ $t('menu.shopping') }}</div>
|
||||
<div v-else></div>
|
||||
</v-list-subheader>
|
||||
|
||||
<v-list-item :title="$t('menu.products')" prepend-icon="mdi-store" to="/" link />
|
||||
<v-list-item :title="$t('menu.basket')" to="/basket" link >
|
||||
<template v-slot:prepend>
|
||||
|
||||
@@ -7,5 +7,5 @@ export class ProductModel {
|
||||
price: number = 0
|
||||
discount: number = 0
|
||||
rating: number = 1
|
||||
imageUrl: string = ""
|
||||
images: Array<string> = [""]
|
||||
}
|
||||
@@ -5,9 +5,9 @@ export class ProductWithCategoryModel {
|
||||
brand: string
|
||||
name: string
|
||||
description: string = ""
|
||||
category: CategoryModel
|
||||
category: CategoryModel = new CategoryModel()
|
||||
price: number = 0
|
||||
discount: number = 0
|
||||
rating: number = 1
|
||||
imageUrl: string = ""
|
||||
images: Array<string> = [""]
|
||||
}
|
||||
@@ -25,7 +25,7 @@ export const useProductStore = defineStore("productStore", {
|
||||
},
|
||||
|
||||
async filterProducts() {
|
||||
if (this.filteredCategory.id == -1) {
|
||||
if (this.filteredCategory.id == -1 || this.filteredCategory.id == 0) {
|
||||
this.filteredProducts = this.products
|
||||
} else {
|
||||
this.filteredProducts = this.products.filter((product: ProductWithCategoryModel) =>
|
||||
|
||||
@@ -1,58 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { SortOrder } from '@/data/enums/sortOrderEnum';
|
||||
import { useCategoryStore } from '@/data/stores/categoryStore';
|
||||
import { useProductStore } from '@/data/stores/productStore';
|
||||
|
||||
const productStore = useProductStore()
|
||||
const categoryStore = useCategoryStore()
|
||||
const sortOrderItems = Object.values(SortOrder)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-card>
|
||||
<v-card-title>
|
||||
<div v-if="productStore.filteredProducts.length == 1">
|
||||
{{ productStore.filteredProducts.length }} {{ $t('product.product') }}
|
||||
</div>
|
||||
<div v-else>
|
||||
{{ productStore.filteredProducts.length }} {{ $t('product.products') }}
|
||||
</div>
|
||||
</v-card-title>
|
||||
<v-container class="pb-0">
|
||||
<v-row>
|
||||
<v-spacer />
|
||||
<v-col class="d-flex justify-center align-center">
|
||||
<v-checkbox :label="$t('offers')" v-model="productStore.onlyDiscounts" />
|
||||
</v-col>
|
||||
|
||||
<v-col class="d-flex justify-left align-center">
|
||||
<v-select
|
||||
:items="categoryStore.categories"
|
||||
:label="$t('categories')"
|
||||
v-model="productStore.filteredCategory"
|
||||
>
|
||||
<template v-slot:item="{ props, item }">
|
||||
<v-list-item v-bind="props" :prepend-icon="item.raw.icon" :title="item.raw.name" />
|
||||
</template>
|
||||
|
||||
<template v-slot:selection="{ item }">
|
||||
<v-list-item :prepend-icon="item.raw.icon" :title="item.raw.name" />
|
||||
</template>
|
||||
</v-select>
|
||||
</v-col>
|
||||
|
||||
<v-col class="d-flex justify-left align-center">
|
||||
<v-select :label="$t('sortBy')" :items="sortOrderItems" v-model="productStore.sortOrder" >
|
||||
<template v-slot:item="{ props, item }">
|
||||
<v-list-item v-bind="props" :title="item.title" />
|
||||
</template>
|
||||
|
||||
<template v-slot:selection="{ item }">
|
||||
<v-list-item :title="item.title" />
|
||||
</template>
|
||||
</v-select>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-container>
|
||||
</v-card>
|
||||
</template>
|
||||
52
software/src/pages/productsPage/filterNavDrawer.vue
Normal file
52
software/src/pages/productsPage/filterNavDrawer.vue
Normal file
@@ -0,0 +1,52 @@
|
||||
<script setup lang="ts">
|
||||
import { SortOrder } from '@/data/enums/sortOrderEnum';
|
||||
import { useCategoryStore } from '@/data/stores/categoryStore';
|
||||
import { useProductStore } from '@/data/stores/productStore';
|
||||
|
||||
const productStore = useProductStore()
|
||||
const categoryStore = useCategoryStore()
|
||||
const sortOrderItems = Object.values(SortOrder)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-navigation-drawer location="right" width="300" permanent>
|
||||
<v-list>
|
||||
<v-list-subheader>Filter</v-list-subheader>
|
||||
|
||||
<v-list-item>
|
||||
<v-checkbox :label="$t('offers')" v-model="productStore.onlyDiscounts" />
|
||||
</v-list-item>
|
||||
|
||||
<v-list-item>
|
||||
<v-select
|
||||
:items="categoryStore.categories"
|
||||
:label="$t('categories')"
|
||||
v-model="productStore.filteredCategory"
|
||||
>
|
||||
<template v-slot:item="{ props, item }">
|
||||
<v-list-item v-bind="props" :prepend-icon="item.raw.icon" :title="item.raw.name" />
|
||||
</template>
|
||||
|
||||
<template v-slot:selection="{ item }">
|
||||
<v-list-item :prepend-icon="item.raw.icon" :title="item.raw.name" />
|
||||
</template>
|
||||
</v-select>
|
||||
</v-list-item>
|
||||
|
||||
<v-divider />
|
||||
<v-list-subheader>Sort</v-list-subheader>
|
||||
|
||||
<v-list-item>
|
||||
<v-select :label="$t('sortBy')" :items="sortOrderItems" v-model="productStore.sortOrder" >
|
||||
<template v-slot:item="{ props, item }">
|
||||
<v-list-item v-bind="props" :title="item.title" />
|
||||
</template>
|
||||
|
||||
<template v-slot:selection="{ item }">
|
||||
<v-list-item :title="item.title" />
|
||||
</template>
|
||||
</v-select>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-navigation-drawer>
|
||||
</template>
|
||||
@@ -1,25 +1,17 @@
|
||||
<script setup lang="ts">
|
||||
import productCard from "./productCard.vue"
|
||||
import productDetails from "./productDetails.vue"
|
||||
import filterBar from "./filterBar.vue"
|
||||
import { ref, watch } from "vue";
|
||||
import { FilterModel } from "@/data/models/filterModel";
|
||||
import { useProductStore } from "@/data/stores/productStore";
|
||||
import { ProductWithCategoryModel } from "@/data/models/productWithCategoryModel";
|
||||
import alertBanner from "@/components/alertBanner.vue";
|
||||
import filterNavDrawer from "./filterNavDrawer.vue";
|
||||
|
||||
const productStore = useProductStore()
|
||||
|
||||
const showProductDetails = ref(false)
|
||||
const dialogProduct = ref(new ProductWithCategoryModel())
|
||||
|
||||
const sortBy: Array<FilterModel> = [
|
||||
{ icon: "mdi-sort-ascending", name: "Price: Low to high" },
|
||||
{ icon: "mdi-sort-descending", name: "Price: High to low" },
|
||||
{ icon: "mdi-sort-alphabetical-ascending", name: "Name: A to Z" },
|
||||
{ icon: "mdi-sort-alphabetical-descending", name: "Name: Z to A" },
|
||||
]
|
||||
|
||||
function showProductDialog(product: ProductWithCategoryModel) {
|
||||
dialogProduct.value = product
|
||||
showProductDetails.value = true
|
||||
@@ -37,11 +29,7 @@ watch(() => productStore.onlyDiscounts, async () => { productStore.filterProduct
|
||||
<alert-banner />
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col>
|
||||
<filter-bar />
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-row dense>
|
||||
<v-col
|
||||
v-if="productStore.filteredProducts.length > 0"
|
||||
@@ -63,6 +51,8 @@ watch(() => productStore.onlyDiscounts, async () => { productStore.filterProduct
|
||||
</v-row>
|
||||
</v-container>
|
||||
|
||||
<filter-nav-drawer />
|
||||
|
||||
<product-details
|
||||
v-model="showProductDetails"
|
||||
:product="dialogProduct"
|
||||
|
||||
@@ -12,7 +12,7 @@ defineProps({
|
||||
<template>
|
||||
<v-card link>
|
||||
<v-img
|
||||
:src="'http://127.0.0.1:3000/static/' + product.imageUrl"
|
||||
:src="'http://127.0.0.1:3000/static/' + product.images[0]"
|
||||
cover
|
||||
max-height="200"
|
||||
class="align-end text-white"
|
||||
@@ -55,7 +55,6 @@ defineProps({
|
||||
<div class="ml-4 mb-1 bg-red">-{{ product.discount }} %</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
@@ -1,19 +1,23 @@
|
||||
<script setup lang="ts">
|
||||
import { VNumberInput } from 'vuetify/labs/VNumberInput'
|
||||
import { ModelRef, ref } from 'vue';
|
||||
import { ModelRef, ref, watch } from 'vue';
|
||||
import { useBasketStore } from '@/data/stores/basketStore';
|
||||
import { calcProductPrice, productToBasketItem } from '@/scripts/productScripts';
|
||||
import ActionDialog from '@/components/actionDialog.vue'
|
||||
import { ProductWithCategoryModel } from '@/data/models/productWithCategoryModel';
|
||||
import { BasketItemModel } from '@/data/models/basketItemModel';
|
||||
|
||||
const props = defineProps({
|
||||
product: {
|
||||
type: ProductWithCategoryModel,
|
||||
default: new ProductWithCategoryModel()
|
||||
}
|
||||
})
|
||||
|
||||
const showDialog: ModelRef<boolean> = defineModel()
|
||||
const nrOfArticles = ref(1)
|
||||
const basketStore = useBasketStore()
|
||||
|
||||
const props = defineProps({
|
||||
product: ProductWithCategoryModel
|
||||
})
|
||||
const selectedImage = ref("")
|
||||
|
||||
function addProductToBasket() {
|
||||
basketStore.addItemToBasket(
|
||||
@@ -23,69 +27,101 @@ function addProductToBasket() {
|
||||
nrOfArticles.value = 1
|
||||
showDialog.value = false
|
||||
}
|
||||
|
||||
watch(() => props.product.images, () => {
|
||||
selectedImage.value = 'http://localhost:3000/static/' + props.product.images[0]
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<action-dialog
|
||||
:title="product.name"
|
||||
:subtitle="product.brand"
|
||||
:image-url="'http://127.0.0.1:3000/static/' + product.imageUrl"
|
||||
:title="product.brand + ': ' + product.name"
|
||||
:icon="product.category.icon"
|
||||
:subtitle="product.category.name"
|
||||
v-model="showDialog"
|
||||
>
|
||||
<template #content>
|
||||
<v-row>
|
||||
<v-col>
|
||||
<v-icon :icon="product.category.icon" />
|
||||
{{ product.category.name }}
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-container>
|
||||
<v-row>
|
||||
<!-- Image col -->
|
||||
<v-col>
|
||||
<v-row>
|
||||
<v-col>
|
||||
<v-img :src="selectedImage" max-height="600" />
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-row>
|
||||
<v-col class="text-h6">
|
||||
{{ $t("product.description") }}
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col v-for="image in product.images">
|
||||
<v-card width="60" @click="selectedImage = 'http://localhost:3000/static/' + image" >
|
||||
<v-img :src="'http://localhost:3000/static/' + image" max-height="60" />
|
||||
</v-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-row>
|
||||
<v-col class="text-body-1">
|
||||
{{ product.description }}
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-col>
|
||||
|
||||
<v-divider class="my-4" />
|
||||
|
||||
<v-row>
|
||||
<v-col>
|
||||
<div class="d-flex align-center flex-column my-auto">
|
||||
<div class="text-h3"> {{ product.rating }} <span class="text-h6 ml-n3">/5</span> </div>
|
||||
<v-rating :model-value="product.rating" color="yellow-darken-3" half-increments disabled />
|
||||
</div>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<!-- Product description col -->
|
||||
<v-col>
|
||||
<v-row>
|
||||
<v-col class="text-h6">
|
||||
{{ $t("product.description") }}
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-divider class="my-4" />
|
||||
<v-row>
|
||||
<v-col class="text-body-1">
|
||||
{{ product.description }}
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-row>
|
||||
<v-col>
|
||||
<v-number-input
|
||||
:reverse="false"
|
||||
controlVariant="default"
|
||||
:label="$t('quantity')"
|
||||
:hideInput="false"
|
||||
:inset="false"
|
||||
v-model="nrOfArticles"
|
||||
:min="1"
|
||||
:max="10"
|
||||
density="comfortable"
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-divider class="my-4" />
|
||||
|
||||
<v-row>
|
||||
<v-col>
|
||||
<div class="d-flex align-center flex-column my-auto">
|
||||
<div class="text-h3"> {{ product.rating }} <span class="text-h6 ml-n3">/5</span> </div>
|
||||
<v-rating :model-value="product.rating" color="yellow-darken-3" half-increments disabled />
|
||||
</div>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-divider class="my-4" />
|
||||
|
||||
<v-row>
|
||||
<v-col>
|
||||
<v-number-input
|
||||
:reverse="false"
|
||||
controlVariant="default"
|
||||
:label="$t('quantity')"
|
||||
:hideInput="false"
|
||||
:inset="false"
|
||||
v-model="nrOfArticles"
|
||||
variant="outlined"
|
||||
:min="1"
|
||||
:max="10"
|
||||
density="comfortable"
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-row>
|
||||
<v-col class="d-flex align-end flex-column my-auto">
|
||||
<div v-if="product.discount == 0" class="text-h3">{{ product.price }} €</div>
|
||||
<div v-else class="d-flex align-center flex-column my-auto">
|
||||
<div class="text-h3">
|
||||
<span class="text-red-lighten-1"> {{ calcProductPrice(product, nrOfArticles) }} €</span>
|
||||
<span class="text-h6 ml-2 text-decoration-line-through">{{ product.price }} € </span>
|
||||
<span class="text-h6 ml-4 mb-1 bg-red">-{{ product.discount }} %</span>
|
||||
</div>
|
||||
</div>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-container>
|
||||
|
||||
<v-row>
|
||||
<v-col class="d-flex align-center flex-column my-auto text-h3">
|
||||
{{ calcProductPrice(product, nrOfArticles) }} €
|
||||
</v-col>
|
||||
</v-row>
|
||||
</template>
|
||||
|
||||
<template #actions>
|
||||
|
||||
Reference in New Issue
Block a user