Improved user feedback system, improved Product detail view

This commit is contained in:
2024-09-20 12:57:19 +02:00
parent ed264ff026
commit 54d13686cf
24 changed files with 1113 additions and 602 deletions

View File

@@ -1,8 +1,9 @@
import { Table, Column, Model, BelongsTo, ForeignKey, HasMany } from 'sequelize-typescript';
import { Table, Column, Model, BelongsTo, ForeignKey, HasMany, Unique } from 'sequelize-typescript';
import { Product } from './product.model';
@Table
export class Category extends Model {
@Unique
@Column
name: string

View File

@@ -14,33 +14,22 @@ account.post("/login", (req: Request, res: Response) => {
if (account != null) {
if (account.password == req.body.password) {
// Status: 200 OK
res.status(200).json({
loginSuccessful: true,
account: account,
message: ""
}).send()
res.status(200).json(account).send()
} else {
// Status: 401 Unauthorized
res.status(401).json({
loginSuccessful: false,
account: null,
message: "Wrong password"
}).send()
res.status(401).send()
}
} else {
// Status: 401 Unauthorized
res.status(401).json({
loginSuccessful: false,
userId: -1,
message: "Username doesn't exists"
}).send()
// Status: 400 Bad request
res.status(400).send()
}
}
)
})
// Creating a new user
account.post("/register", (req: Request, res: Response) => {
account.post("/", (req: Request, res: Response) => {
// Check if username is valid
if (!validateString(req.body.username, 4))
{
// Status: 400 Bad request
@@ -48,25 +37,25 @@ account.post("/register", (req: Request, res: Response) => {
message: "Username too short!"
}).send()
}
else if (!validateString(req.body.password, 8))
// Check if password is valid
if (!validateString(req.body.password, 8))
{
// Status: 400 Bad request
res.status(400).json({
message: "Password too short!"
}).send()
}
else
{
Account.create(req.body)
.then(account => {
res.status(200).json(account).send()
}).catch(reason => {
// Status: 400 Bad request
res.status(400).json({
message: reason
}).send()
})
}
// Create account
Account.create(req.body)
.then(account => {
// Status: 201 Created
res.status(201).json(account).send()
}).catch(reason => {
// Status: 409 Conflict
res.status(409).send()
})
})
account.patch("/", (req: Request, res: Response) => {
@@ -75,9 +64,11 @@ account.patch("/", (req: Request, res: Response) => {
where: { id: req.body.id }
})
.then(account => {
res.status(200).send()
// Status: 200 OK
res.status(200).json(account).send()
})
.catch(error => {
// Status: 400 Bad request
res.status(400).json({
message: error
}).send()

View File

@@ -10,7 +10,7 @@ category.get("/", (req: Request, res: Response, next: NextFunction) => {
res.status(200).json(categories).send()
})
.catch(error => {
res.status(400)//.json({ message: error }).send()
res.status(400)
})
})
@@ -36,7 +36,7 @@ category.delete("/:id", (req: Request, res: Response, next: NextFunction) => {
res.status(200).send()
})
.catch(error => {
res.status(406).json({
res.status(400).json({
message: error
}).send()
})

View File

@@ -14,7 +14,7 @@ order.get("/:id", (req: Request, res: Response) => {
]
})
.then(orders => {
res.status(200).send(orders)
res.status(200).json(orders).send()
})
})

View File

@@ -18,7 +18,7 @@ product.get("/", (req: Request, res: Response) => {
product.get("/:productId", (req: Request, res: Response) => {
Product.findByPk(req.params.productId)
.then(product => {
res.status(200).json(product)
res.status(200).json(product).send()
})
})
@@ -26,7 +26,7 @@ product.get("/:productId", (req: Request, res: Response) => {
product.post("/", (req: Request, res: Response) => {
Product.create(req.body)
.then(product => {
res.status(200).send()
res.status(201).json(product).send()
})
.catch(error => {
res.status(400).json({
@@ -41,10 +41,10 @@ product.delete("/:id", (req: Request, res: Response) => {
where: { id: req.params.id }
})
.then(product => {
res.status(200).send()
res.status(200).json(product).send()
})
.catch(error => {
res.status(406).json({
res.status(400).json({
message: error
}).send()
})

File diff suppressed because it is too large Load Diff

View File

@@ -21,7 +21,7 @@
"@vueuse/core": "^11.0.3",
"axios": "^1.7.7",
"body-parser": "^1.20.2",
"concurrently": "^8.2.2",
"concurrently": "^9.0.1",
"cors": "^2.8.5",
"express": "^4.19.2",
"pinia": "^2.2.2",
@@ -33,7 +33,7 @@
"vue": "^3.4.29",
"vue-i18n": "^10.0.0",
"vue-router": "^4.4.3",
"vuetify": "^3.7.1",
"vuetify": "^3.7.2",
"wait-on": "^8.0.0"
},
"devDependencies": {

View File

@@ -14,14 +14,22 @@ defineProps({
</script>
<template>
<v-dialog max-width="800" v-model="showDialog">
<v-card :title="title" >
<v-img v-if="imageUrl != ''" :src="imageUrl" max-height="300" />
<v-card-text>
<slot name="content"></slot>
</v-card-text>
<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-card-actions>
<slot name="actions"></slot>
</v-card-actions>

View File

@@ -3,13 +3,17 @@ import { AccountModel } from "../models/accountModel"
const BASE_URL = "http://localhost:3000/accounts"
export async function login(username: string, password: string) {
export async function loginAccount(username: string, password: string) {
return await axios.post(BASE_URL + "/login", {
username: username,
password: password
})
}
export async function register(account: AccountModel) {
return await axios.post(BASE_URL + "/register", account)
export async function registerAccount(account: AccountModel) {
return await axios.post(BASE_URL, account)
}
export async function updateAccount(account: AccountModel) {
return await axios.patch(BASE_URL, account)
}

View File

@@ -1,8 +1,79 @@
export enum BannerStateEnum {
DATABASERESETSUCCESSFUL,
////////// System feedback //////////
// Some error
ERROR,
WRONGLOGIN,
LOGINSUCCESSFUL,
REGISTERSUCCESSFUL,
USERNAMEINUSE
BASKETPRODUCTADDED,
BASKETPRODUCTREMOVED,
////////// API Endpoint /api //////////
// Status: 200 OK
DATABASERESETSUCCESSFUL,
////////// API Endpoint /accounts //////////
// Status: 200 OK
ACCOUNTLOGINSUCCESSFUL,
// Status: 400 Bad request
ACCOUNTLOGINERROR,
// Status: 401 Unauthorized
ACCOUNTLOGINWRONGLOGIN,
// Status: 201 Created
ACCOUNTREGISTERSUCCESSFUL,
// Status: 400 Bad request
ACCOUNTREGISTERERROR,
// Status: 409 Conflict
ACCOUNTREGISTERUSERNAMEINUSE,
// Status: 200 OK
ACCOUNTUPDATESUCCESSFUL,
// No status code, runs in local cache
ACCOUNTLOGOUTSUCCESSFUL,
////////// API Endpoint /categories //////////
// Status: 201 Created
CATEGORYCREATESUCCESSFUL,
// Status: 200 OK
CATEGORYDELETESUCESSFUL,
// Status: 400 Bad request
CATEGORYCREATEERROR,
// Status: 400 Bad request
CATEGORYDELETEERROR,
////////// API Endpoint /orders //////////
// Status: 201 Created
ORDERPLACESUCCESSFUL,
////////// API Endpoint /products //////////
// Status: 201 Created
PRODUCTCREATESUCCESSFUL,
// Status: 400 Bad request
PRODUCTCREATEERROR,
// Status: 200 OK
PRODUCTDELETESUCCESSFUL,
// Status: 400 Bad request
PRODUCTDELETEERROR
}

View File

@@ -2,6 +2,8 @@ import { defineStore } from "pinia";
import { useLocalStorage } from "@vueuse/core";
import { calcProductPrice } from "@/scripts/productScripts";
import { BasketItemModel } from "../models/basketItemModel";
import { useFeedbackStore } from "./feedbackStore";
import { BannerStateEnum } from "../enums/bannerStateEnum";
export const useBasketStore = defineStore('basket', {
state: () => ({
@@ -22,12 +24,18 @@ export const useBasketStore = defineStore('basket', {
actions: {
removeItemFromBasket(item: BasketItemModel) {
const feedbackStore = useFeedbackStore()
feedbackStore.changeBanner(BannerStateEnum.BASKETPRODUCTREMOVED)
this.itemsInBasket = this.itemsInBasket.filter((basketItemModel: BasketItemModel) =>
basketItemModel.productId != item.productId
)
},
addItemToBasket(item: BasketItemModel) {
const feedbackStore = useFeedbackStore()
feedbackStore.changeBanner(BannerStateEnum.BASKETPRODUCTADDED)
// Product is already in the basket, increase number of items
if (this.itemsInBasket.find((basketItem) => basketItem.productId == item.productId)) {
this.itemsInBasket.find((basketItem) => basketItem.productId == item.productId).quantity += item.quantity

View File

@@ -21,37 +21,116 @@ export const useFeedbackStore = defineStore("feedbackStore", {
actions: {
changeBanner(bannerState: BannerStateEnum) {
switch (bannerState) {
////////// System feedback //////////
case BannerStateEnum.ERROR: {
this.title = this.i18n.t('bannerMessages.error'); break;
}
case BannerStateEnum.BASKETPRODUCTADDED: {
this.title = this.i18n.t('bannerMessages.basketProductAdded'); break;
}
case BannerStateEnum.BASKETPRODUCTREMOVED: {
this.title = this.i18n.t("bannerMessages.basketProductRemoved"); break;
}
////////// API Endpoint /api //////////
case BannerStateEnum.DATABASERESETSUCCESSFUL: {
this.title = this.i18n.t('bannerMessages.databaseResetSuccessful'); break;
}
case BannerStateEnum.LOGINSUCCESSFUL: {
////////// API Endpoint /accounts //////////
case BannerStateEnum.ACCOUNTLOGINSUCCESSFUL: {
this.title = this.i18n.t('bannerMessages.loginSuccessful'); break;
}
case BannerStateEnum.WRONGLOGIN: {
case BannerStateEnum.ACCOUNTLOGINWRONGLOGIN: {
this.title = this.i18n.t('bannerMessages.wrongLogin'); break;
}
case BannerStateEnum.REGISTERSUCCESSFUL: {
case BannerStateEnum.ACCOUNTLOGINERROR: {
this.title = this.i18n.t('bannerMessages.error'); break;
}
case BannerStateEnum.ACCOUNTREGISTERSUCCESSFUL: {
this.title = this.i18n.t("bannerMessages.registerSuccessful"); break;
}
case BannerStateEnum.USERNAMEINUSE: {
case BannerStateEnum.ACCOUNTREGISTERUSERNAMEINUSE: {
this.title = this.i18n.t("bannerMessages.usernameInUse"); break;
}
case BannerStateEnum.ACCOUNTUPDATESUCCESSFUL: {
this.title = this.i18n.t("bannerMessages.accountUpdated"); break;
}
case BannerStateEnum.ACCOUNTLOGOUTSUCCESSFUL: {
this.title = this.i18n.t('bannerMessages.logoutSuccessful'); break;
}
////////// API Endpoint /categories //////////
case BannerStateEnum.CATEGORYCREATESUCCESSFUL: {
this.title = this.i18n.t('bannerMessages.categoryCreateSuccessful'); break;
}
case BannerStateEnum.CATEGORYDELETESUCESSFUL: {
this.title = this.i18n.t('bannerMessages.categoryDeleteSuccessful'); break;
}
case BannerStateEnum.CATEGORYCREATEERROR: {
this.title = this.i18n.t('bannerMessages.categoryCreateError'); break;
}
case BannerStateEnum.CATEGORYDELETEERROR: {
this.title = this.i18n.t('bannerMessages.categoryDeleteError'); break;
}
////////// API Endpoint /orders //////////
case BannerStateEnum.ORDERPLACESUCCESSFUL: {
this.title = this.i18n.t('bannerMessages.orderPlaceSuccessfull'); break;
}
////////// API Endpoint /products //////////
case BannerStateEnum.PRODUCTCREATESUCCESSFUL: {
this.title = this.i18n.t('bannerMessages.productCreateSuccessful'); break;
}
case BannerStateEnum.PRODUCTCREATEERROR: {
this.title = this.i18n.t('bannerMessages.productCreateError'); break;
}
case BannerStateEnum.PRODUCTDELETESUCCESSFUL: {
this.title = this.i18n.t('bannerMessages.productDeleteSuccessful'); break;
}
case BannerStateEnum.PRODUCTDELETEERROR: {
this.title = this.i18n.t('bannerMessages.productDeleteError'); break;
}
}
switch (bannerState) {
case BannerStateEnum.ERROR:
case BannerStateEnum.WRONGLOGIN:
case BannerStateEnum.USERNAMEINUSE:
case BannerStateEnum.ACCOUNTLOGINERROR:
case BannerStateEnum.ACCOUNTLOGINWRONGLOGIN:
case BannerStateEnum.ACCOUNTREGISTERERROR:
case BannerStateEnum.ACCOUNTREGISTERUSERNAMEINUSE:
case BannerStateEnum.CATEGORYCREATEERROR:
case BannerStateEnum.CATEGORYDELETEERROR:
case BannerStateEnum.PRODUCTDELETESUCCESSFUL:
case BannerStateEnum.PRODUCTDELETEERROR:
this.color = "red"
this.icon = "mdi-alert-circle"
break;
case BannerStateEnum.DATABASERESETSUCCESSFUL:
case BannerStateEnum.LOGINSUCCESSFUL:
case BannerStateEnum.REGISTERSUCCESSFUL:
case BannerStateEnum.ACCOUNTLOGINSUCCESSFUL:
case BannerStateEnum.ACCOUNTREGISTERSUCCESSFUL:
case BannerStateEnum.ACCOUNTUPDATESUCCESSFUL:
case BannerStateEnum.ACCOUNTLOGOUTSUCCESSFUL:
case BannerStateEnum.CATEGORYCREATESUCCESSFUL:
case BannerStateEnum.CATEGORYDELETESUCESSFUL:
case BannerStateEnum.ORDERPLACESUCCESSFUL:
case BannerStateEnum.PRODUCTCREATESUCCESSFUL:
case BannerStateEnum.PRODUCTCREATEERROR:
this.color = "green"
this.icon = "mdi-check-circle"
break

View File

@@ -3,7 +3,7 @@ import { useLocalStorage } from "@vueuse/core";
import { ThemeEnum } from "../enums/themeEnums";
import { LanguageEnum } from "../enums/languageEnum";
import { AccountModel } from "../models/accountModel";
import { login, register } from "../api/accountApi";
import { loginAccount, registerAccount, updateAccount } from "../api/accountApi";
import { useFeedbackStore } from "./feedbackStore";
import { BannerStateEnum } from "../enums/bannerStateEnum";
@@ -19,44 +19,60 @@ export const useUserStore = defineStore('userStore', {
async login(username: string, password: string) {
const feedbackStore = useFeedbackStore()
await login(username, password)
await loginAccount(username, password)
.then(result => {
if (result.data.loginSuccessful) {
this.userAccount = result.data.account
this.loggedIn = true
this.userAccount = result.data.account
this.loggedIn = true
feedbackStore.changeBanner(BannerStateEnum.LOGINSUCCESSFUL)
}
feedbackStore.changeBanner(BannerStateEnum.ACCOUNTLOGINSUCCESSFUL)
})
.catch(error => {
this.userAccount = new AccountModel()
this.loggedIn = false
feedbackStore.changeBanner(BannerStateEnum.WRONGLOGIN)
if (error.status == 400) {
feedbackStore.changeBanner(BannerStateEnum.ACCOUNTLOGINERROR)
} else if (error.status == 401) {
feedbackStore.changeBanner(BannerStateEnum.ACCOUNTLOGINWRONGLOGIN)
}
})
},
async registerAccount(account: AccountModel) {
async registerAccount(userAccount: AccountModel) {
const feedbackStore = useFeedbackStore()
await register(account)
await registerAccount(userAccount)
.then(res => {
console.log(res)
if (res.status == 200) {
feedbackStore.changeBanner(BannerStateEnum.REGISTERSUCCESSFUL)
if (res.status == 201) {
feedbackStore.changeBanner(BannerStateEnum.ACCOUNTREGISTERSUCCESSFUL)
}
})
.catch((error) => {
console.log(error)
if (error.status == 400) {
feedbackStore.changeBanner(BannerStateEnum.USERNAMEINUSE)
feedbackStore.changeBanner(BannerStateEnum.ACCOUNTREGISTERERROR)
} else if (error.status == 409) {
feedbackStore.changeBanner(BannerStateEnum.ACCOUNTREGISTERUSERNAMEINUSE)
}
})
},
async updateAccount() {
const feedbackStore = useFeedbackStore()
await updateAccount(this.userAccount)
.then(res => {
if (res.status == 200) {
feedbackStore.changeBanner(BannerStateEnum.ACCOUNTUPDATESUCCESSFUL)
}
})
},
logout() {
const feedbackStore = useFeedbackStore()
this.userAccount = new AccountModel()
this.loggedIn = false
feedbackStore.changeBanner(BannerStateEnum.ACCOUNTLOGOUTSUCCESSFUL)
}
}
})

View File

@@ -25,7 +25,8 @@
"productName": "Produkt Name",
"brand": "Marke",
"productPrice": "Einzelpreis",
"category": "Kategorie"
"category": "Kategorie",
"description": "Beschreibung"
},
"offers": "Angebote",
"categories": "Kategorien",
@@ -64,7 +65,20 @@
"error": "Es ist ein Fehler aufgetreten...",
"databaseResetSuccessful": "Datenbank erfolgreich zurück gesetzt!",
"registerSuccessful": "Account erfolgreich erstellt!",
"usernameInUse": "Der Accountname ist bereits vergeben!"
"usernameInUse": "Der Accountname ist bereits vergeben!",
"accountUpdated": "Account erfolgreich aktualisiert",
"logoutSuccessful": "Logout erfolgreich",
"categoryCreateSuccessful": "Kategorie hinzugefügt",
"categoryDeleteSuccessful": "Kategorie gelöscht",
"categoryCreateError": "Fehler beim Erstellen der Kategorie",
"categoryDeleteError": "Fehler beim Löschen der Kategorie",
"orderPlaceSuccessfull": "Bestellung erfolgreich aufgegeben",
"productCreateSuccessful": "Produkt hinzugefügt",
"productCreateError": "Fehler beim Erstellen des Produktes",
"productDeleteSuccessful": "Produkt erfolgreich entfernt",
"productDeleteError": "Fehler beim Entfernen des Produktes",
"basketProductAdded": "Artikel zum Warenkorb hinzugefügt",
"basketProductRemoved": "Artikel aus Warenkorb entfernt"
},
"oclock": "Uhr"
}

View File

@@ -25,7 +25,8 @@
"productName": "Product name",
"brand": "Brand",
"productPrice": "Unit price",
"category": "Category"
"category": "Category",
"description": "Description"
},
"offers": "Offers",
"categories": "Categories",
@@ -64,7 +65,20 @@
"error": "Some error occurred...",
"databaseResetSuccessful": "Database reset successful!",
"usernameInUse": "Username always in use!",
"registerSuccessful": "Account successfully created!"
"registerSuccessful": "Account successfully created!",
"accountUpdated": "Account successfully updated",
"logoutSuccessful": "Logout successful",
"categoryCreateSuccessful": "Category created",
"categoryDeleteSuccessful": "Category deleted",
"categoryCreateError": "Error on creating category",
"categoryDeleteError": "Error on deleting category",
"orderPlaceSuccessfull": "Order successfully placed",
"productCreateSuccessful": "Product created",
"productCreateError": "Error on product creation",
"productDeleteSuccessful": "Product successfully deleted",
"productDeleteError": "Error on product delete",
"basketProductAdded": "Add product to basket",
"basketProductRemoved": "Product removed from basket"
},
"oclock": "o'clock"
}

View File

@@ -0,0 +1,77 @@
<script setup lang="ts">
import { useUserStore } from '@/data/stores/userStore';
const userStore = useUserStore()
</script>
<template>
<v-card title="Account">
<v-container>
<v-row>
<v-col>
<v-text-field
:label="$t('account.username')"
v-model="userStore.userAccount.username"
disabled
/>
</v-col>
<v-col>
<v-text-field
:label="$t('account.password')"
v-model="userStore.userAccount.password"
type="password"
/>
</v-col>
</v-row>
<v-row>
<v-col>
<v-text-field
:label="$t('userInfo.firstName')"
v-model="userStore.userAccount.firstName"
/>
</v-col>
<v-col>
<v-text-field
:label="$t('userInfo.lastName')"
v-model="userStore.userAccount.lastName"
/>
</v-col>
</v-row>
<v-row>
<v-col>
<v-text-field
:label="$t('userInfo.street')"
v-model="userStore.userAccount.street"
/>
</v-col>
<v-col>
<v-text-field
:label="$t('userInfo.houseNumber')"
v-model="userStore.userAccount.houseNumber"
/>
</v-col>
</v-row>
<v-row>
<v-col>
<v-text-field
:label="$t('userInfo.postalCode')"
v-model="userStore.userAccount.postalCode"
/>
</v-col>
<v-col>
<v-text-field
:label="$t('userInfo.city')"
v-model="userStore.userAccount.city"
/>
</v-col>
</v-row>
</v-container>
<v-card-actions>
<v-btn @click="userStore.updateAccount()" >Save</v-btn>
</v-card-actions>
</v-card>
</template>

View File

@@ -1,84 +1,19 @@
<script setup lang="ts">
import { useUserStore } from '@/data/stores/userStore';
const userStore = useUserStore()
import alertBanner from '@/components/alertBanner.vue';
import accountDataCard from './accountDataCard.vue';
</script>
<template>
<v-container max-width="1000">
<v-row>
<v-col>
<v-card title="Account">
<v-container>
<v-row>
<v-col>
<v-text-field
:label="$t('account.username')"
v-model="userStore.userAccount.username"
disabled
/>
</v-col>
<v-col>
<v-text-field
:label="$t('account.password')"
v-model="userStore.userAccount.password"
type="password"
/>
</v-col>
</v-row>
<alert-banner />
</v-col>
</v-row>
<v-row>
<v-col>
<v-text-field
:label="$t('userInfo.firstName')"
v-model="userStore.userAccount.firstName"
/>
</v-col>
<v-col>
<v-text-field
:label="$t('userInfo.lastName')"
v-model="userStore.userAccount.lastName"
/>
</v-col>
</v-row>
<v-row>
<v-col>
<v-text-field
:label="$t('userInfo.street')"
v-model="userStore.userAccount.street"
/>
</v-col>
<v-col>
<v-text-field
:label="$t('userInfo.houseNumber')"
v-model="userStore.userAccount.houseNumber"
/>
</v-col>
</v-row>
<v-row>
<v-col>
<v-text-field
:label="$t('userInfo.postalCode')"
v-model="userStore.userAccount.postalCode"
/>
</v-col>
<v-col>
<v-text-field
:label="$t('userInfo.city')"
v-model="userStore.userAccount.city"
/>
</v-col>
</v-row>
</v-container>
<v-card-actions>
<!-- todo -->
<v-btn >Save</v-btn>
</v-card-actions>
</v-card>
<v-row>
<v-col>
<account-data-card />
</v-col>
</v-row>
</v-container>

View File

@@ -1,12 +1,18 @@
<script setup lang="ts">
import { useBasketStore } from '@/data/stores/basketStore';
import productsTable from './productsTable.vue';
import alertBanner from '@/components/alertBanner.vue';
const basketStore = useBasketStore()
</script>
<template>
<v-container max-width="1000">
<v-row>
<v-col>
<alert-banner />
</v-col>
</v-row>
<v-row>
<v-col>
<v-card :title="$t('menu.basket')" prepend-icon="mdi-cart">

View File

@@ -2,15 +2,13 @@
import productCard from "./productCard.vue"
import productDetails from "./productDetails.vue"
import filterBar from "./filterBar.vue"
import { Ref, ref, watch } from "vue";
import { CategoryModel } from "@/data/models/categoryModel";
import { ref, watch } from "vue";
import { FilterModel } from "@/data/models/filterModel";
import { useProductStore } from "@/data/stores/productStore";
import { useCategoryStore } from "@/data/stores/categoryStore";
import { ProductWithCategoryModel } from "@/data/models/productWithCategoryModel";
import alertBanner from "@/components/alertBanner.vue";
const productStore = useProductStore()
const categoryStore = useCategoryStore()
const showProductDetails = ref(false)
const dialogProduct = ref(new ProductWithCategoryModel())
@@ -34,6 +32,11 @@ watch(() => productStore.onlyDiscounts, async () => { productStore.filterProduct
<template>
<v-container>
<v-row>
<v-col>
<alert-banner />
</v-col>
</v-row>
<v-row>
<v-col>
<filter-bar />

View File

@@ -1,12 +1,11 @@
<script setup lang="ts">
import { VNumberInput } from 'vuetify/labs/VNumberInput'
import { ProductModel } from '@/data/models/productModel';
import { CategoryModel } from '@/data/models/categoryModel';
import { ModelRef, ref } 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 showDialog: ModelRef<boolean> = defineModel()
const nrOfArticles = ref(1)
@@ -17,7 +16,10 @@ const props = defineProps({
})
function addProductToBasket() {
// todo basketStore.addItemToBasket(productToBasketItem(props.product, props.productCategory, nrOfArticles.value))
basketStore.addItemToBasket(
new BasketItemModel()
)
basketStore.addItemToBasket(productToBasketItem(props.product, nrOfArticles.value))
nrOfArticles.value = 1
showDialog.value = false
}
@@ -31,17 +33,38 @@ function addProductToBasket() {
v-model="showDialog"
>
<template #content>
<v-row>
<!-- todo <v-col>
<v-icon :icon="productCategory.icon" />
{{ productCategory.name }}
</v-col> -->
</v-row>
<v-row>
<v-col>
<v-icon :icon="product.category.icon" />
{{ product.category.name }}
</v-col>
</v-row>
<v-row>
<v-col class="text-h6">
{{ $t("product.description") }}
</v-col>
</v-row>
<v-row>
<v-col class="text-body-1">
{{ product.description }}
</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
@@ -56,10 +79,10 @@ function addProductToBasket() {
density="comfortable"
/>
</v-col>
</v-row>
<v-spacer />
<v-col cols="2" class="justify-center d-flex">
<v-row>
<v-col class="d-flex align-center flex-column my-auto text-h3">
{{ calcProductPrice(product, nrOfArticles) }}
</v-col>
</v-row>

View File

@@ -29,7 +29,7 @@ export function calcPrice(price: number, discount: number = 0, quantity: number
*
* @returns BasketItemModel
*/
export function productToBasketItem(product: ProductModel, productCategory: CategoryModel, quantity: number): BasketItemModel {
export function productToBasketItem(product: ProductWithCategoryModel, quantity: number): BasketItemModel {
let result = new BasketItemModel()
result.productId = product.id
@@ -38,8 +38,8 @@ export function productToBasketItem(product: ProductModel, productCategory: Cate
result.brand = product.brand
result.discount = product.discount
result.name = product.name
result.categoryName = productCategory.name
result.categoryIcon = productCategory.icon
result.categoryName = product.category.name
result.categoryIcon = product.category.icon
return result
}