Add subcription feature

This commit is contained in:
Urtzi Alfaro
2026-01-13 22:22:38 +01:00
parent b931a5c45e
commit 6ddf608d37
61 changed files with 7915 additions and 1238 deletions

View File

@@ -7,6 +7,7 @@ import {
subscriptionService,
type PlanMetadata,
type SubscriptionTier,
type BillingCycle,
SUBSCRIPTION_TIERS
} from '../../api';
import { getRegisterUrl } from '../../utils/navigation';
@@ -23,6 +24,8 @@ interface SubscriptionPricingCardsProps {
pilotTrialMonths?: number;
showComparison?: boolean;
className?: string;
billingCycle?: BillingCycle;
onBillingCycleChange?: (cycle: BillingCycle) => void;
}
export const SubscriptionPricingCards: React.FC<SubscriptionPricingCardsProps> = ({
@@ -33,14 +36,19 @@ export const SubscriptionPricingCards: React.FC<SubscriptionPricingCardsProps> =
pilotCouponCode,
pilotTrialMonths = 3,
showComparison = false,
className = ''
className = '',
billingCycle: externalBillingCycle,
onBillingCycleChange
}) => {
const { t } = useTranslation('subscription');
const [plans, setPlans] = useState<Record<SubscriptionTier, PlanMetadata> | null>(null);
const [billingCycle, setBillingCycle] = useState<BillingCycle>('monthly');
const [internalBillingCycle, setInternalBillingCycle] = useState<BillingCycle>('monthly');
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
// Use external billing cycle if provided, otherwise use internal state
const billingCycle = externalBillingCycle || internalBillingCycle;
useEffect(() => {
loadPlans();
}, []);
@@ -145,34 +153,48 @@ export const SubscriptionPricingCards: React.FC<SubscriptionPricingCardsProps> =
</Card>
)}
{/* Billing Cycle Toggle */}
<div className="flex justify-center mb-8">
<div className="inline-flex rounded-lg border-2 border-[var(--border-primary)] p-1 bg-[var(--bg-secondary)]">
<button
onClick={() => setBillingCycle('monthly')}
className={`px-6 py-2 rounded-md text-sm font-semibold transition-all ${
billingCycle === 'monthly'
? 'bg-[var(--color-primary)] text-white shadow-md'
: 'text-[var(--text-secondary)] hover:bg-[var(--bg-primary)] hover:text-[var(--text-primary)]'
}`}
>
{t('billing.monthly', 'Mensual')}
</button>
<button
onClick={() => setBillingCycle('yearly')}
className={`px-6 py-2 rounded-md text-sm font-semibold transition-all flex items-center gap-2 ${
billingCycle === 'yearly'
? 'bg-[var(--color-primary)] text-white shadow-md'
: 'text-[var(--text-secondary)] hover:bg-[var(--bg-primary)] hover:text-[var(--text-primary)]'
}`}
>
{t('billing.yearly', 'Anual')}
<span className="text-xs font-bold text-green-600 dark:text-green-400">
{t('billing.save_percent', 'Ahorra 17%')}
</span>
</button>
{/* Billing Cycle Toggle - Only show if not externally controlled */}
{!externalBillingCycle && (
<div className="flex justify-center mb-8">
<div className="inline-flex rounded-lg border-2 border-[var(--border-primary)] p-1 bg-[var(--bg-secondary)]">
<button
onClick={() => {
const newCycle: BillingCycle = 'monthly';
setInternalBillingCycle(newCycle);
if (onBillingCycleChange) {
onBillingCycleChange(newCycle);
}
}}
className={`px-6 py-2 rounded-md text-sm font-semibold transition-all ${
billingCycle === 'monthly'
? 'bg-[var(--color-primary)] text-white shadow-md'
: 'text-[var(--text-secondary)] hover:bg-[var(--bg-primary)] hover:text-[var(--text-primary)]'
}`}
>
{t('billing.monthly', 'Mensual')}
</button>
<button
onClick={() => {
const newCycle: BillingCycle = 'yearly';
setInternalBillingCycle(newCycle);
if (onBillingCycleChange) {
onBillingCycleChange(newCycle);
}
}}
className={`px-6 py-2 rounded-md text-sm font-semibold transition-all flex items-center gap-2 ${
billingCycle === 'yearly'
? 'bg-[var(--color-primary)] text-white shadow-md'
: 'text-[var(--text-secondary)] hover:bg-[var(--bg-primary)] hover:text-[var(--text-primary)]'
}`}
>
{t('billing.yearly', 'Anual')}
<span className="text-xs font-bold text-green-600 dark:text-green-400">
{t('billing.save_percent', 'Ahorra 17%')}
</span>
</button>
</div>
</div>
</div>
)}
{/* Simplified Plans Grid */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8 items-start lg:items-stretch">
@@ -186,7 +208,7 @@ export const SubscriptionPricingCards: React.FC<SubscriptionPricingCardsProps> =
const CardWrapper = mode === 'landing' ? Link : 'div';
const isCurrentPlan = mode === 'settings' && selectedPlan === tier;
const cardProps = mode === 'landing'
? { to: getRegisterUrl(tier) }
? { to: getRegisterUrl(tier, billingCycle) }
: mode === 'selection' || (mode === 'settings' && !isCurrentPlan)
? { onClick: () => handlePlanAction(tier, plan) }
: {};