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 (
|
2026-05-26 16:09:26 +00:00
|
|
|
<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">
|
2026-02-24 20:44:16 +00:00
|
|
|
{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>
|
|
|
|
|
|
2026-05-26 16:09:26 +00:00
|
|
|
<h3 className="text-lg font-semibold text-gray-900 mb-2 line-clamp-2 dark:text-gray-100">{translate('::' + product.name)}</h3>
|
2026-02-24 20:44:16 +00:00
|
|
|
|
2026-05-26 16:09:26 +00:00
|
|
|
<p className="text-gray-600 text-sm mb-4 line-clamp-3 dark:text-gray-300">{translate('::' + product.description)}</p>
|
2026-02-24 20:44:16 +00:00
|
|
|
|
|
|
|
|
{/* Quantity and Yearly Savings above price */}
|
|
|
|
|
<div className="mb-4 space-y-3">
|
|
|
|
|
{product.isQuantityBased && (
|
|
|
|
|
<div className="flex items-center space-x-3">
|
2026-05-26 16:09:26 +00:00
|
|
|
<span className="text-sm font-medium text-gray-700 dark:text-gray-300">
|
2026-02-24 20:44:16 +00:00
|
|
|
{translate('::Public.products.quantity')}
|
|
|
|
|
</span>
|
|
|
|
|
<div className="flex items-center space-x-2">
|
|
|
|
|
<button
|
|
|
|
|
onClick={() => setQuantity(Math.max(1, quantity - 1))}
|
2026-05-26 16:09:26 +00:00
|
|
|
className="p-1 rounded-md border border-gray-300 hover:bg-gray-50 transition-colors dark:border-gray-700 dark:text-gray-200 dark:hover:bg-gray-800"
|
2026-02-24 20:44:16 +00:00
|
|
|
>
|
|
|
|
|
<FaMinus className="w-4 h-4" />
|
|
|
|
|
</button>
|
2026-05-26 16:09:26 +00:00
|
|
|
<span className="w-12 text-center font-medium dark:text-gray-100">{quantity}</span>
|
2026-02-24 20:44:16 +00:00
|
|
|
<button
|
|
|
|
|
onClick={() => setQuantity(quantity + 1)}
|
2026-05-26 16:09:26 +00:00
|
|
|
className="p-1 rounded-md border border-gray-300 hover:bg-gray-50 transition-colors dark:border-gray-700 dark:text-gray-200 dark:hover:bg-gray-800"
|
2026-02-24 20:44:16 +00:00
|
|
|
>
|
|
|
|
|
<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">
|
2026-05-26 16:09:26 +00:00
|
|
|
<div className="text-2xl font-bold text-gray-900 dark:text-gray-100">
|
2026-02-24 20:44:16 +00:00
|
|
|
{formatPrice(getCurrentPrice())}
|
2026-05-26 16:09:26 +00:00
|
|
|
<span className="text-sm font-normal text-gray-500 ml-1 dark:text-gray-400">{getUnitText()}</span>
|
2026-02-24 20:44:16 +00:00
|
|
|
</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-05-26 16:09:26 +00:00
|
|
|
<span className="text-sm font-normal text-gray-500 ml-1 dark:text-gray-400">{getPeriodText()}</span>
|
2026-02-24 20:44:16 +00:00
|
|
|
</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
|
2026-05-26 16:09:26 +00:00
|
|
|
? 'bg-gray-400 text-gray-700 cursor-not-allowed dark:bg-gray-700 dark:text-gray-400'
|
2026-02-24 20:44:16 +00:00
|
|
|
: '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>
|
|
|
|
|
)
|
|
|
|
|
}
|