sozsoft-platform/ui/src/components/orders/ProductCard.tsx
2026-08-14 17:03:34 +03:00

211 lines
7.2 KiB
TypeScript

import React, { useState } from 'react'
import { FaPlus, FaMinus } from 'react-icons/fa'
import { BillingCycle, ProductDto } from '@/proxy/order/models'
import { BasketData } from '@/utils/basketUtils'
import { useLocalization } from '@/utils/hooks/useLocalization'
import { Button } from '@/components/ui'
interface ProductCardProps {
product: ProductDto
globalBillingCycle: BillingCycle
globalPeriod: number
addItem: (product: ProductDto, quantity: number, billingCycle: BillingCycle) => void
basketData: BasketData
}
export const ProductCard: React.FC<ProductCardProps> = ({
product,
globalBillingCycle,
globalPeriod,
addItem,
basketData,
}) => {
const [quantity, setQuantity] = useState(1)
const { translate } = useLocalization()
const isInBasket = basketData.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 getVatRate = () => product.vatRate ?? 0.2
const getTotalPrice = () => {
return getCurrentPrice() * quantity * globalPeriod
}
const getVatAmount = () => {
return getTotalPrice() * getVatRate()
}
const getTotalPriceWithVat = () => {
return getTotalPrice() + getVatAmount()
}
const handleAddToBasket = () => {
if (!product.isQuantityBased && isInBasket) {
return
}
const quantityToAdd = !product.isQuantityBased ? 1 : quantity
addItem(product, quantityToAdd, globalBillingCycle)
setQuantity(1)
}
const getUnitText = () => {
switch (product.unit) {
case 'monthly':
return globalBillingCycle === 'monthly'
? translate('::App.PublicProducts.MonthPeriod')
: translate('::App.PublicProducts.YearPeriod')
case 'yearly':
return translate('::App.PublicProducts.YearPeriod')
default:
return ''
}
}
const getPeriodText = () => {
const periodText =
globalBillingCycle === 'monthly'
? translate('::App.Platform.Months')
: translate('::App.Platform.Years')
return globalPeriod > 1 ? ` (${globalPeriod} ${periodText})` : ''
}
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 dark:border-gray-700 dark:bg-gray-900 dark:shadow-gray-950/40">
{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 dark:text-gray-100">
{translate('::' + product.name)}
</h3>
<p className="text-gray-600 text-sm mb-4 line-clamp-3 dark:text-gray-300">
{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 dark:text-gray-300">
{translate('::App.PublicProducts.Quantity')}
</span>
<div className="flex items-center space-x-2">
<Button
onClick={() => setQuantity(Math.max(1, quantity - 1))}
icon={<FaMinus className="w-4 h-4" />}
variant="default"
size="xs"
/>
<span className="w-12 text-center font-sm dark:text-gray-100">{quantity}</span>
<Button
onClick={() => setQuantity(quantity + 1)}
icon={<FaPlus className="w-4 h-4" />}
variant="default"
size="xs"
/>
</div>
</div>
)}
{globalBillingCycle === 'yearly' && product.monthlyPrice && product.yearlyPrice && (
<div className="text-sm text-emerald-600 font-medium">
{translate('::App.ProductsSavings.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 dark:text-gray-100">
{formatPrice(getCurrentPrice())}
<span className="text-sm font-normal text-gray-500 ml-1 dark:text-gray-400">
{getUnitText()}
</span>
</div>
<div className="mt-1 text-xs text-gray-500 dark:text-gray-400">
{translate('::App.PublicProducts.Kdv')} (%{(getVatRate() * 100).toFixed(0)}):{' '}
{formatPrice(getVatAmount())}
</div>
{globalPeriod > 1 && (
<div className="text-lg font-semibold text-blue-600 mt-1">
{translate('::App.Listform.ListformField.Total')}{' '}
{formatPrice(getTotalPriceWithVat())}
<span className="text-sm font-normal text-gray-500 ml-1 dark:text-gray-400">
{getPeriodText()}
</span>
</div>
)}
{globalPeriod <= 1 && (
<div className="text-lg font-semibold text-blue-600 mt-1">
{translate('::App.PublicProducts.Kdvdahil')} {formatPrice(getTotalPriceWithVat())}
</div>
)}
</div>
{(() => {
const isNonQuantityProduct = !product.isQuantityBased
const isDisabled = isNonQuantityProduct && isInBasket
return (
<Button
onClick={handleAddToBasket}
disabled={isDisabled}
block
variant="solid"
size="md"
className={!isDisabled ? 'hover:scale-[1.02] active:scale-[0.98]' : ''}
>
{isDisabled
? translate('::App.PublicProducts.InBasket')
: translate('::App.PublicProducts.AddToBasket')}
</Button>
)
})()}
</div>
</div>
</div>
)
}