Backend: - Add TenantCustomTier model for per-tenant feature overrides - Update EntitlementService to check custom tier before plan features - Add custom_tier action on TenantViewSet (GET/PUT/DELETE) - Add Celery task for grace period management (30-day expiry) Frontend: - Add DynamicFeaturesEditor component for dynamic feature management - Fix BusinessEditModal to load features from plan defaults when no custom tier - Update limits (max_users, max_resources, etc.) to use featureValues - Remove outdated canonical feature check from FeaturePicker (removes warning icons) - Add useBillingPlans hook for accessing billing system data - Add custom tier API functions to platform.ts Features now follow consistent rules: - Load from plan defaults when no custom tier exists - Load from custom tier when one exists - Reset to plan defaults when plan changes - Save to custom tier on edit 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
78 lines
2.8 KiB
TypeScript
78 lines
2.8 KiB
TypeScript
import React from 'react';
|
|
import { useTranslation } from 'react-i18next';
|
|
import DynamicPricingCards from '../../components/marketing/DynamicPricingCards';
|
|
import FeatureComparisonTable from '../../components/marketing/FeatureComparisonTable';
|
|
import FAQAccordion from '../../components/marketing/FAQAccordion';
|
|
import CTASection from '../../components/marketing/CTASection';
|
|
|
|
const PricingPage: React.FC = () => {
|
|
const { t } = useTranslation();
|
|
|
|
return (
|
|
<div className="bg-gray-50 dark:bg-gray-900 min-h-screen">
|
|
{/* Header */}
|
|
<div className="pt-24 pb-12 text-center px-4 sm:px-6 lg:px-8">
|
|
<h1 className="text-4xl font-bold text-gray-900 dark:text-white mb-4">
|
|
{t('marketing.pricing.title')}
|
|
</h1>
|
|
<p className="text-xl text-gray-600 dark:text-gray-400 max-w-2xl mx-auto">
|
|
{t('marketing.pricing.subtitle')}
|
|
</p>
|
|
</div>
|
|
|
|
{/* Dynamic Pricing Cards */}
|
|
<div className="pb-20">
|
|
<DynamicPricingCards />
|
|
</div>
|
|
|
|
{/* Feature Comparison Table */}
|
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 pb-20">
|
|
<h2 className="text-3xl font-bold text-center text-gray-900 dark:text-white mb-4">
|
|
{t('marketing.pricing.featureComparison.title', 'Compare Plans')}
|
|
</h2>
|
|
<p className="text-center text-gray-600 dark:text-gray-400 mb-12 max-w-2xl mx-auto">
|
|
{t(
|
|
'marketing.pricing.featureComparison.subtitle',
|
|
'See exactly what you get with each plan'
|
|
)}
|
|
</p>
|
|
<div className="bg-white dark:bg-gray-800 rounded-2xl shadow-sm border border-gray-200 dark:border-gray-700 p-6">
|
|
<FeatureComparisonTable />
|
|
</div>
|
|
</div>
|
|
|
|
{/* FAQ Section */}
|
|
<div className="max-w-3xl mx-auto px-4 sm:px-6 lg:px-8 pb-20">
|
|
<h2 className="text-3xl font-bold text-center text-gray-900 dark:text-white mb-12">
|
|
{t('marketing.pricing.faq.title')}
|
|
</h2>
|
|
<FAQAccordion
|
|
items={[
|
|
{
|
|
question: t('marketing.pricing.faq.needPython.question'),
|
|
answer: t('marketing.pricing.faq.needPython.answer'),
|
|
},
|
|
{
|
|
question: t('marketing.pricing.faq.exceedLimits.question'),
|
|
answer: t('marketing.pricing.faq.exceedLimits.answer'),
|
|
},
|
|
{
|
|
question: t('marketing.pricing.faq.customDomain.question'),
|
|
answer: t('marketing.pricing.faq.customDomain.answer'),
|
|
},
|
|
{
|
|
question: t('marketing.pricing.faq.dataSafety.question'),
|
|
answer: t('marketing.pricing.faq.dataSafety.answer'),
|
|
},
|
|
]}
|
|
/>
|
|
</div>
|
|
|
|
{/* CTA */}
|
|
<CTASection />
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default PricingPage;
|