Files
smoothschedule/frontend/src/pages/TrialExpired.tsx
poduck c7f241b30a feat(i18n): Comprehensive internationalization of frontend components and pages
Translate all hardcoded English strings to use i18n translation keys:

Components:
- TransactionDetailModal: payment details, refunds, technical info
- ConnectOnboarding/ConnectOnboardingEmbed: Stripe Connect setup
- StripeApiKeysForm: API key management
- DomainPurchase: domain registration flow
- Sidebar: navigation labels
- Schedule/Sidebar, PendingSidebar: scheduler UI
- MasqueradeBanner: masquerade status
- Dashboard widgets: metrics, capacity, customers, tickets
- Marketing: PricingTable, PluginShowcase, BenefitsSection
- ConfirmationModal, ServiceList: common UI

Pages:
- Staff: invitation flow, role management
- Customers: form placeholders
- Payments: transactions, payouts, billing
- BookingSettings: URL and redirect configuration
- TrialExpired: upgrade prompts and features
- PlatformSettings, PlatformBusinesses: admin UI
- HelpApiDocs: API documentation

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-03 21:40:54 -05:00

227 lines
11 KiB
TypeScript

import React from 'react';
import { useNavigate, useOutletContext } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { Clock, ArrowRight, Check, X, CreditCard, TrendingDown, AlertTriangle } from 'lucide-react';
import { User, Business } from '../types';
/**
* TrialExpired Page
* Shown when a business trial has expired and they need to either upgrade or downgrade to free tier
*/
const TrialExpired: React.FC = () => {
const { t } = useTranslation();
const navigate = useNavigate();
const { user, business } = useOutletContext<{ user: User; business: Business }>();
const isOwner = user.role === 'owner';
// Feature comparison based on tier
const getTierFeatures = (tier: string | undefined) => {
switch (tier) {
case 'Professional':
return [
{ name: t('trialExpired.features.professional.unlimitedAppointments'), included: true },
{ name: t('trialExpired.features.professional.onlineBooking'), included: true },
{ name: t('trialExpired.features.professional.emailNotifications'), included: true },
{ name: t('trialExpired.features.professional.smsReminders'), included: true },
{ name: t('trialExpired.features.professional.customBranding'), included: true },
{ name: t('trialExpired.features.professional.advancedAnalytics'), included: true },
{ name: t('trialExpired.features.professional.paymentProcessing'), included: true },
{ name: t('trialExpired.features.professional.prioritySupport'), included: true },
];
case 'Business':
return [
{ name: t('trialExpired.features.business.everythingInProfessional'), included: true },
{ name: t('trialExpired.features.business.multipleLocations'), included: true },
{ name: t('trialExpired.features.business.teamManagement'), included: true },
{ name: t('trialExpired.features.business.apiAccess'), included: true },
{ name: t('trialExpired.features.business.customDomain'), included: true },
{ name: t('trialExpired.features.business.whiteLabel'), included: true },
{ name: t('trialExpired.features.business.accountManager'), included: true },
];
case 'Enterprise':
return [
{ name: t('trialExpired.features.enterprise.everythingInBusiness'), included: true },
{ name: t('trialExpired.features.enterprise.unlimitedUsers'), included: true },
{ name: t('trialExpired.features.enterprise.customIntegrations'), included: true },
{ name: t('trialExpired.features.enterprise.slaGuarantee'), included: true },
{ name: t('trialExpired.features.enterprise.customContracts'), included: true },
{ name: t('trialExpired.features.enterprise.phoneSupport'), included: true },
{ name: t('trialExpired.features.enterprise.onPremise'), included: true },
];
default:
return [];
}
};
const freeTierFeatures = [
{ name: t('trialExpired.features.free.upTo50Appointments'), included: true },
{ name: t('trialExpired.features.free.basicOnlineBooking'), included: true },
{ name: t('trialExpired.features.free.emailNotifications'), included: true },
{ name: t('trialExpired.features.free.smsReminders'), included: false },
{ name: t('trialExpired.features.free.customBranding'), included: false },
{ name: t('trialExpired.features.free.advancedAnalytics'), included: false },
{ name: t('trialExpired.features.free.paymentProcessing'), included: false },
{ name: t('trialExpired.features.free.prioritySupport'), included: false },
];
const paidTierFeatures = getTierFeatures(business.plan);
const handleUpgrade = () => {
navigate('/payments');
};
const handleDowngrade = () => {
if (window.confirm(t('trialExpired.confirmDowngrade'))) {
// TODO: Implement downgrade to free tier API call
console.log('Downgrading to free tier...');
}
};
return (
<div className="min-h-screen bg-gradient-to-br from-gray-50 to-gray-100 dark:from-gray-900 dark:to-gray-800 flex items-center justify-center p-4">
<div className="max-w-4xl w-full">
{/* Main Card */}
<div className="bg-white dark:bg-gray-800 rounded-2xl shadow-2xl overflow-hidden">
{/* Header */}
<div className="bg-gradient-to-r from-red-500 to-orange-500 text-white p-8 text-center">
<div className="flex justify-center mb-4">
<div className="bg-white/20 backdrop-blur-sm rounded-full p-4">
<Clock size={48} />
</div>
</div>
<h1 className="text-3xl font-bold mb-2">{t('trialExpired.title')}</h1>
<p className="text-white/90 text-lg">
{t('trialExpired.subtitle', {
plan: business.plan,
date: business.trialEnd ? new Date(business.trialEnd).toLocaleDateString() : 'N/A'
})}
</p>
</div>
{/* Content */}
<div className="p-8">
<div className="mb-8">
<h2 className="text-2xl font-semibold text-gray-900 dark:text-white mb-4">
{t('trialExpired.whatHappensNow')}
</h2>
<p className="text-gray-600 dark:text-gray-300 mb-4">
{t('trialExpired.twoOptions')}
</p>
</div>
{/* Feature Comparison */}
<div className="grid md:grid-cols-2 gap-6 mb-8">
{/* Free Tier Card */}
<div className="border-2 border-gray-200 dark:border-gray-700 rounded-xl p-6">
<div className="flex items-center justify-between mb-4">
<h3 className="text-xl font-semibold text-gray-900 dark:text-white">{t('trialExpired.freePlan')}</h3>
<span className="px-3 py-1 bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300 rounded-full text-sm font-medium">
{t('trialExpired.pricePerMonth')}
</span>
</div>
<ul className="space-y-3 mb-6">
{freeTierFeatures.map((feature, idx) => (
<li key={idx} className="flex items-start gap-2">
{feature.included ? (
<Check size={20} className="text-green-500 flex-shrink-0 mt-0.5" />
) : (
<X size={20} className="text-gray-400 flex-shrink-0 mt-0.5" />
)}
<span className={feature.included ? 'text-gray-700 dark:text-gray-300' : 'text-gray-400 dark:text-gray-500 line-through'}>
{feature.name}
</span>
</li>
))}
</ul>
{isOwner && (
<button
onClick={handleDowngrade}
className="w-full px-4 py-3 bg-gray-200 dark:bg-gray-700 text-gray-700 dark:text-gray-300 rounded-lg hover:bg-gray-300 dark:hover:bg-gray-600 transition-colors font-medium flex items-center justify-center gap-2"
>
<TrendingDown size={20} />
{t('trialExpired.downgradeToFree')}
</button>
)}
</div>
{/* Paid Tier Card */}
<div className="border-2 border-blue-500 dark:border-blue-400 rounded-xl p-6 bg-blue-50/50 dark:bg-blue-900/20 relative">
<div className="absolute top-4 right-4">
<span className="px-3 py-1 bg-blue-500 text-white rounded-full text-xs font-bold uppercase tracking-wide">
{t('trialExpired.recommended')}
</span>
</div>
<div className="mb-4">
<h3 className="text-xl font-semibold text-gray-900 dark:text-white mb-1">
{business.plan} {t('common.plan', { defaultValue: 'Plan' })}
</h3>
<p className="text-sm text-gray-600 dark:text-gray-400">{t('trialExpired.continueWhereYouLeftOff')}</p>
</div>
<ul className="space-y-3 mb-6">
{paidTierFeatures.slice(0, 8).map((feature, idx) => (
<li key={idx} className="flex items-start gap-2">
<Check size={20} className="text-blue-500 flex-shrink-0 mt-0.5" />
<span className="text-gray-700 dark:text-gray-300">{feature.name}</span>
</li>
))}
{paidTierFeatures.length > 8 && (
<li className="text-sm text-gray-500 dark:text-gray-400 italic">
{t('trialExpired.moreFeatures', { count: paidTierFeatures.length - 8 })}
</li>
)}
</ul>
{isOwner && (
<button
onClick={handleUpgrade}
className="w-full px-4 py-3 bg-gradient-to-r from-blue-600 to-blue-500 text-white rounded-lg hover:from-blue-700 hover:to-blue-600 transition-all font-medium flex items-center justify-center gap-2 shadow-lg shadow-blue-500/30"
>
<CreditCard size={20} />
{t('trialExpired.upgradeNow')}
<ArrowRight size={20} />
</button>
)}
</div>
</div>
{/* Important Notice */}
<div className="bg-yellow-50 dark:bg-yellow-900/20 border border-yellow-200 dark:border-yellow-800 rounded-lg p-4">
<div className="flex gap-3">
<div className="flex-shrink-0">
<AlertTriangle className="h-5 w-5 text-yellow-600 dark:text-yellow-500" />
</div>
<div className="flex-1">
<p className="text-sm text-yellow-800 dark:text-yellow-200 font-medium">
{isOwner
? t('trialExpired.ownerLimitedFunctionality')
: t('trialExpired.nonOwnerContactOwner')}
</p>
</div>
</div>
</div>
{!isOwner && (
<div className="mt-6 text-center">
<p className="text-gray-600 dark:text-gray-400">
{t('trialExpired.businessOwner')} <span className="font-semibold text-gray-900 dark:text-white">{business.name}</span>
</p>
</div>
)}
</div>
</div>
{/* Footer */}
<div className="text-center mt-6">
<p className="text-gray-600 dark:text-gray-400 text-sm">
{t('trialExpired.supportQuestion')}{' '}
<a href={`mailto:${t('trialExpired.supportEmail')}`} className="text-blue-600 dark:text-blue-400 hover:underline">
{t('trialExpired.supportEmail')}
</a>
</p>
</div>
</div>
</div>
);
};
export default TrialExpired;