Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,12 @@
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;

import java.time.Instant;

Expand Down Expand Up @@ -57,6 +59,25 @@ public final ErrorResponse handleInsufficientStockException(InsufficientStockExc
return buildErrorResponse(HttpStatus.CONFLICT, e.getMessage());
}

@ExceptionHandler(MethodArgumentTypeMismatchException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ResponseBody
public final ErrorResponse handleTypeMismatch(MethodArgumentTypeMismatchException e) {
log.debug("MethodArgumentTypeMismatchException occurred", e);
String message = String.format("Invalid value '%s' for parameter '%s'.", e.getValue(), e.getName());
return buildErrorResponse(HttpStatus.BAD_REQUEST, message);
}

@ExceptionHandler(MethodArgumentNotValidException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ResponseBody
public final ErrorResponse handleValidationExceptions(MethodArgumentNotValidException e) {
log.debug("MethodArgumentNotValidException occurred", e);
String message = e.getBindingResult().getAllErrors().get(0).getDefaultMessage();
return buildErrorResponse(HttpStatus.BAD_REQUEST, message);
}


@Data
@Getter
@Setter
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,6 @@ public class ProductSummaryResponse {
private Integer roastLevel;
private Double averageRating;
private Long reviewCount;
private Integer stock;
private Boolean isActive;
}
Original file line number Diff line number Diff line change
Expand Up @@ -135,11 +135,6 @@ public Order addItemToOrder(Long orderId, OrderItemRequest itemRequest)
throw new IllegalOrderArgumentException("Order status is not pending!");
}

if (!product.getIsActive()) {
log.error("Product is not active: {}", product.getName());
throw new IllegalOrderArgumentException("Product is not active!");
}

// Create OrderItem with price from product
OrderItem orderItem = new OrderItem();
orderItem.setOrder(savedOrder);
Expand Down
4 changes: 0 additions & 4 deletions frontend/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -205,10 +205,6 @@ Both contexts persist data to `localStorage` for a seamless user experience acro

See `package.json` for the complete list of dependencies.

## 🚧 Development Status

Currently, the frontend is using **mock data** from `src/utils/DummyProducts.ts`. The API integration layer in `src/services/main.api.ts` is prepared for backend integration but returns dummy data for now.

### Next Steps

- Connect to actual Spring Boot backend API
Expand Down
3 changes: 2 additions & 1 deletion frontend/public/locales/en/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,8 @@
"quantity": "Quantity",
"writeReviewPlaceholder": "Write your review here...",
"reviewerName": "Your Name",
"reviewerNamePlaceholder": "Enter your name"
"reviewerNamePlaceholder": "Enter your name",
"outOfStock": "Out of Stock"
},
"title": "Match My Coffee",
"description": "Find your perfect coffee match based on your preferences and taste!",
Expand Down
23 changes: 19 additions & 4 deletions frontend/src/components/cart/CheckoutModal.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import axios from 'axios';
import { useState } from 'react';
import { Alert, Modal, Spinner } from 'react-bootstrap';
import { useTranslation } from 'react-i18next';
Expand Down Expand Up @@ -25,7 +26,9 @@ function CheckoutModal(props: CheckoutModalProps) {
const [error, setError] = useState<string | null>(null);

const handleConfirm = async () => {
if (!order) return;
if (!order) {
return;
}

setIsSubmitting(true);
setError(null);
Expand All @@ -41,9 +44,21 @@ function CheckoutModal(props: CheckoutModalProps) {
}, 5000);
onHide();
} catch (err) {
console.error('Failed to submit order:', err);
const errorMessage = (err as { response?: { data?: { error?: string } } })?.response?.data?.error;
setError(errorMessage ?? t('checkout.orderSubmitError') ?? 'Failed to submit order. Please try again.');
if (axios.isAxiosError(err)) {
const responseData = err.response?.data as
| { data: { message: string } }
| { message: string }
| undefined;

const backendMessage =
(responseData && 'data' in responseData && responseData.data?.message) ??
(responseData && 'message' in responseData && responseData.message) ??
err.message;

setError(backendMessage || 'An unexpected error occurred');
} else {
setError('An unexpected error occurred');
}
} finally {
setIsSubmitting(false);
}
Expand Down
34 changes: 12 additions & 22 deletions frontend/src/components/common/ProductCard.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
import { Button, ButtonGroup, Card, Dropdown, DropdownButton } from 'react-bootstrap';
import { Button, Card } from 'react-bootstrap';
import { useTranslation } from 'react-i18next';
import { IoCloseSharp } from 'react-icons/io5';
import { Link } from 'react-router-dom';

import { useProductActions } from '../../hooks/useProductActions.ts';
import type { ProductSummary } from '../../types/ProductsType.ts';
import AddToCartButton from '../common/AddToCartButton.tsx';
import AddToFavoritesButton from '../common/AddToFavoritesButton.tsx';
import StarRating from '../common/StarRating.tsx';
import CustomButtonGroup from '../product/CustomButtonGroup.tsx';
import style from './ProductCard.module.css';

interface ProductTypeProps {
Expand Down Expand Up @@ -53,27 +52,18 @@ function ProductCard(props: ProductTypeProps) {
<StarRating rating={data.averageRating} showCount={data.reviewCount} key={data.id} />
</div>
</Link>
{inCart ? (
<>
<DropdownButton
as={ButtonGroup}
key={'cart-count'}
id={`cart-count`}
variant={'none'}
title={`${t('product.quantity')}: ${currentQuantity}`}
onSelect={handleQuantitySelect}
>
{Array.from({ length: 10 }, (_, i) => i + 1).map((count) => (
<Dropdown.Item key={count} eventKey={count.toString()}>
{count}
</Dropdown.Item>
))}
</DropdownButton>
</>
{data.isActive || data.stock > 0 ? (
<div className={style.buttonGroup}>
<CustomButtonGroup
inCart={inCart}
data={data}
currentQuantity={currentQuantity}
handleQuantitySelect={handleQuantitySelect}
/>
</div>
) : (
<AddToCartButton product={data} variant={'icon-only'} inCart={inCart} />
<div className={style.outOfStockLabel}>{t('product.outOfStock')}</div>
)}
<AddToFavoritesButton product={data} variant={'icon-only'} />
</Card.Body>
</Card>
);
Expand Down
43 changes: 43 additions & 0 deletions frontend/src/components/product/CustomButtonGroup.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { ButtonGroup, Dropdown, DropdownButton } from 'react-bootstrap';
import { useTranslation } from 'react-i18next';

import type { ProductSummary } from '../../types/ProductsType.ts';
import AddToCartButton from '../common/AddToCartButton.tsx';
import AddToFavoritesButton from '../common/AddToFavoritesButton.tsx';

interface CustomButtonGroupProps {
inCart?: boolean;
data: ProductSummary;
currentQuantity: number;
handleQuantitySelect: (eventKey: string | null) => void;
}

function CustomButtonGroup({ inCart, data, currentQuantity, handleQuantitySelect }: CustomButtonGroupProps) {
const { t } = useTranslation();

return (
<>
{inCart ? (
<DropdownButton
as={ButtonGroup}
key={'cart-count'}
id={`cart-count`}
variant={'none'}
title={`${t('product.quantity')}: ${currentQuantity}`}
onSelect={handleQuantitySelect}
>
{Array.from({ length: Math.min(10, data.stock) }, (_, i) => i + 1).map((count) => (
<Dropdown.Item key={count} eventKey={count.toString()}>
{count}
</Dropdown.Item>
))}
</DropdownButton>
) : (
<AddToCartButton product={data} variant={'icon-only'} inCart={inCart} />
)}
<AddToFavoritesButton product={data} variant={'icon-only'} />
</>
);
}

export default CustomButtonGroup;
17 changes: 14 additions & 3 deletions frontend/src/pages/CartCheckoutPage.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import axios from 'axios';
import React, { useContext, useState } from 'react';
import { Alert, Dropdown, DropdownButton, Form } from 'react-bootstrap';
import { useTranslation } from 'react-i18next';
Expand Down Expand Up @@ -105,9 +106,19 @@ function CartCheckoutPage() {
setCreatedOrder(order);
setModalShow(true);
} catch (err) {
console.error('Failed to create order:', err);
const errorMessage = (err as { response?: { data?: { error?: string } } })?.response?.data?.error;
setError(errorMessage ?? t('checkout.orderCreationError') ?? 'Failed to create order. Please try again.');
if (axios.isAxiosError(err)) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const responseData = err.response?.data;
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access
const backendMessage = responseData?.data?.message ?? responseData?.message ?? err.message;

setError(
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
backendMessage ?? t('checkout.orderCreationError') ?? 'Failed to create order. Please try again.',
);
} else {
setError(t('checkout.orderCreationError') ?? 'Failed to create order. Please try again.');
}
} finally {
setIsSubmitting(false);
}
Expand Down
46 changes: 26 additions & 20 deletions frontend/src/pages/ProductPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,10 +51,14 @@ function ProductPage() {
</div>
<div className={style.purchaseSection}>
<h3>{product.price} $</h3>
<div className={style.buttonContainer}>
<AddToCartButton product={product} variant={'with-text'} />
<AddToFavoritesButton product={product} variant={'with-text'} />
</div>
{product.isActive && product.stock > 0 ? (
<div className={style.buttonContainer}>
<AddToCartButton product={product} variant={'with-text'} />
<AddToFavoritesButton product={product} variant={'with-text'} />
</div>
) : (
<h2>{t('product.outOfStock')}</h2>
)}
</div>
</div>
</div>
Expand Down Expand Up @@ -121,23 +125,25 @@ function ProductPage() {
</tbody>
</Table>
</div>
<section id={'reviews'}>
<div className={style.reviewsSection}>
<h2>{t('product.reviews')}</h2>
<AddReview refetchWithInvalidation={refetchWithInvalidation} productId={product.id} />
{Array.isArray(reviews) && reviews.length !== 0
? reviews.map((review, index) => (
<div key={`${review.id}-${index}`} className={style.reviewCard}>
<div className={style.reviewHeader}>
<p>{review.authorName}</p>
<StarRating rating={review.rating} size={20} />
{product.isActive && product.stock > 0 ? (
<section id={'reviews'}>
<div className={style.reviewsSection}>
<h2>{t('product.reviews')}</h2>
<AddReview refetchWithInvalidation={refetchWithInvalidation} productId={product.id} />
{Array.isArray(reviews) && reviews.length !== 0
? reviews.map((review, index) => (
<div key={`${review.id}-${index}`} className={style.reviewCard}>
<div className={style.reviewHeader}>
<p>{review.authorName}</p>
<StarRating rating={review.rating} size={20} />
</div>
<p>{review.comment}</p>
</div>
<p>{review.comment}</p>
</div>
))
: null}
</div>
</section>
))
: null}
</div>
</section>
) : null}
</div>
</div>
);
Expand Down
4 changes: 2 additions & 2 deletions frontend/src/types/ProductsType.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ export interface ProductSummary {
price: number;
imageUrl: string;
isBlend: boolean;
stock: number;
isActive: boolean;

specifications: {
roastLevel: number;
Expand All @@ -17,8 +19,6 @@ export interface ProductSummary {

export interface ProductDetail extends ProductSummary {
description: string;
stock: number;
isActive: boolean;

specifications: {
roastLevel: number;
Expand Down
Loading