sozsoft-platform/ui/src/components/orders/ProductCard.tsx

186 lines
6.6 KiB
TypeScript
Raw Normal View History

2026-02-24 20:44:16 +00:00
import React, { useState } from 'react'
import { FaPlus, FaMinus } from 'react-icons/fa'
import { BillingCycle, Product, ProductDto } from '@/proxy/order/models'
import { CartState } from '@/utils/cartUtils'
import { useLocalization } from '@/utils/hooks/useLocalization'
interface ProductCardProps {
product: ProductDto
globalBillingCycle: BillingCycle
globalPeriod: number
addItem: (product: ProductDto, quantity: number, billingCycle: BillingCycle) => void
cartState: CartState
}
export const ProductCard: React.FC<ProductCardProps> = ({
product,
globalBillingCycle,
globalPeriod,
addItem,
cartState,
}) => {
const [quantity, setQuantity] = useState(1)
const { translate } = useLocalization()
// Direct check with current cart state
const isInCart = cartState.items.some(
(item) => item.product.id === product.id && item.billingCycle === globalBillingCycle,
)
const formatPrice = (price: number) => {
return new Intl.NumberFormat('tr-TR', {
style: 'currency',
currency: 'TRY',
minimumFractionDigits: 0,
}).format(price)
}
const getCurrentPrice = () => {
return globalBillingCycle === 'monthly' ? product.monthlyPrice! : product.yearlyPrice!
}
const getTotalPrice = () => {
return getCurrentPrice() * quantity * globalPeriod
}
const handleAddToCart = () => {
// Non-quantity products should not be added if already in cart
if (!product.isQuantityBased && isInCart) {
return
}
// For non-quantity products, always use quantity 1
const quantityToAdd = !product.isQuantityBased ? 1 : quantity
addItem(product, quantityToAdd, globalBillingCycle)
setQuantity(1)
}
const getUnitText = () => {
switch (product.unit) {
case 'monthly':
return globalBillingCycle === 'monthly'
? translate('::Public.Products.MonthPeriod')
: translate('::Public.Products.YearPeriod')
case 'yearly':
return translate('::Public.Products.YearPeriod')
default:
return ''
}
}
const getPeriodText = () => {
const periodText =
globalBillingCycle === 'monthly'
? translate('::Public.products.billingcycle.month')
: translate('::Public.products.billingcycle.year')
return globalPeriod > 1 ? ` (${globalPeriod} ${periodText})` : ''
}
// Check if product has valid price for current billing cycle
const hasValidPrice = () => {
if (globalBillingCycle === 'monthly' && !product.monthlyPrice) return false
if (globalBillingCycle === 'yearly' && !product.yearlyPrice) return false
return true
}
// If product doesn't have valid price, don't render it at all
if (!hasValidPrice()) {
return null
}
return (
<div className="bg-white rounded-xl shadow-sm border border-gray-200 overflow-hidden hover:shadow-lg transition-all duration-300 group flex flex-col h-full">
{product.imageUrl && (
<div className="aspect-video overflow-hidden">
<img
src={product.imageUrl}
alt={product.name}
className="w-full h-full object-cover group-hover:scale-105 transition-transform duration-300"
/>
</div>
)}
<div className="p-6 flex flex-col flex-1">
<div className="mb-2">
<span className="inline-block px-3 py-1 bg-blue-100 text-blue-800 text-sm font-medium rounded-full">
{translate('::' + product.category)}
</span>
</div>
<h3 className="text-lg font-semibold text-gray-900 mb-2 line-clamp-2">{translate('::' + product.name)}</h3>
<p className="text-gray-600 text-sm mb-4 line-clamp-3">{translate('::' + product.description)}</p>
{/* Quantity and Yearly Savings above price */}
<div className="mb-4 space-y-3">
{product.isQuantityBased && (
<div className="flex items-center space-x-3">
<span className="text-sm font-medium text-gray-700">
{translate('::Public.products.quantity')}
</span>
<div className="flex items-center space-x-2">
<button
onClick={() => setQuantity(Math.max(1, quantity - 1))}
className="p-1 rounded-md border border-gray-300 hover:bg-gray-50 transition-colors"
>
<FaMinus className="w-4 h-4" />
</button>
<span className="w-12 text-center font-medium">{quantity}</span>
<button
onClick={() => setQuantity(quantity + 1)}
className="p-1 rounded-md border border-gray-300 hover:bg-gray-50 transition-colors"
>
<FaPlus className="w-4 h-4" />
</button>
</div>
</div>
)}
{globalBillingCycle === 'yearly' && product.monthlyPrice && product.yearlyPrice && (
<div className="text-sm text-emerald-600 font-medium">
{translate('::Public.products.savings.yearly', {
percent: Math.round((1 - product.yearlyPrice / (product.monthlyPrice * 12)) * 100),
})}
</div>
)}
</div>
{/* Bottom section with consistent height */}
<div className="mt-auto">
<div className="mb-4">
<div className="text-2xl font-bold text-gray-900">
{formatPrice(getCurrentPrice())}
<span className="text-sm font-normal text-gray-500 ml-1">{getUnitText()}</span>
</div>
{globalPeriod > 1 && (
<div className="text-lg font-semibold text-blue-600 mt-1">
2026-03-29 08:59:07 +00:00
{translate('::App.Listform.ListformField.Total')} {formatPrice(getTotalPrice())}
2026-02-24 20:44:16 +00:00
<span className="text-sm font-normal text-gray-500 ml-1">{getPeriodText()}</span>
</div>
)}
</div>
{(() => {
const isNonQuantityProduct = !product.isQuantityBased
const isDisabled = isNonQuantityProduct && isInCart
return (
<button
onClick={handleAddToCart}
disabled={isDisabled}
className={`w-full font-medium py-3 px-4 rounded-lg transition-colors duration-200 transform ${
isDisabled
? 'bg-gray-400 text-gray-700 cursor-not-allowed'
: 'bg-blue-600 hover:bg-blue-700 text-white hover:scale-[1.02] active:scale-[0.98]'
}`}
>
{isDisabled ? translate('::Public.products.inCart') : translate('::Public.products.addToCart')}
</button>
)
})()}
</div>
</div>
</div>
)
}