feature: localization's first implmentation done. all frontend hardcoded text is now localized.
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { Link, useRouter } from '@/i18n/navigation';
|
||||
import { useAuth } from '@/lib/hooks/useAuth';
|
||||
import { authApi } from '@/lib/api/auth';
|
||||
@@ -9,14 +10,16 @@ import { Toast } from '@/components/ui/shared/Toast';
|
||||
import type { SubscriptionAlertData } from '@/types/subscription';
|
||||
|
||||
const PLAN_OPTIONS = [
|
||||
{ id: 'solo', name: 'Solo', maxUsers: 1, price: 19 },
|
||||
{ id: 'small', name: 'Small', maxUsers: 5, price: 49 },
|
||||
{ id: 'medium', name: 'Medium', maxUsers: 10, price: 89 },
|
||||
{ id: 'large', name: 'Large', maxUsers: 15, price: 129 },
|
||||
{ id: 'enterprise', name: 'Enterprise', maxUsers: null, price: 199 },
|
||||
{ id: 'solo', nameKey: 'planSolo' as const, maxUsers: 1, price: 19 },
|
||||
{ id: 'small', nameKey: 'planSmall' as const, maxUsers: 5, price: 49 },
|
||||
{ id: 'medium', nameKey: 'planMedium' as const, maxUsers: 10, price: 89 },
|
||||
{ id: 'large', nameKey: 'planLarge' as const, maxUsers: 15, price: 129 },
|
||||
{ id: 'enterprise', nameKey: 'planEnterprise' as const, maxUsers: null, price: 199 },
|
||||
] as const;
|
||||
|
||||
export default function SubscriptionsSettingsPage() {
|
||||
const t = useTranslations('settings');
|
||||
const tCommon = useTranslations('common');
|
||||
const { currentOrganization } = useAuth();
|
||||
const router = useRouter();
|
||||
const [alert, setAlert] = useState<SubscriptionAlertData | null>(null);
|
||||
@@ -38,13 +41,13 @@ export default function SubscriptionsSettingsPage() {
|
||||
|
||||
if (!currentOrganization) {
|
||||
return (
|
||||
<p className="text-text-secondary text-sm">Loading...</p>
|
||||
<p className="text-text-secondary text-sm">{tCommon('loadingEllipsis')}</p>
|
||||
);
|
||||
}
|
||||
|
||||
if (!currentOrganization.isOwner) {
|
||||
return (
|
||||
<p className="text-text-secondary text-sm">Redirecting...</p>
|
||||
<p className="text-text-secondary text-sm">{tCommon('redirecting')}</p>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -75,55 +78,51 @@ export default function SubscriptionsSettingsPage() {
|
||||
href="/today"
|
||||
className="text-sm text-primary hover:opacity-90"
|
||||
>
|
||||
← Back to app
|
||||
{tCommon('backToApp')}
|
||||
</Link>
|
||||
<h1 className="text-2xl font-semibold text-text-primary mt-4">Subscriptions</h1>
|
||||
<h1 className="text-2xl font-semibold text-text-primary mt-4">{t('subscriptionsTitle')}</h1>
|
||||
<p className="text-text-secondary text-sm mt-2">
|
||||
Your DyoLink workspace plan and seats for{' '}
|
||||
<span className="text-text-primary font-medium">{currentOrganization.name}</span>.
|
||||
Clinic and lab income tracking stays under the sidebar{' '}
|
||||
<span className="text-text-primary">Billing</span> tab.
|
||||
{t('subscriptionsSubtitle', { orgName: currentOrganization.name })}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="surface-card p-6 space-y-4">
|
||||
{!hasActiveSubscription && (
|
||||
<div className="rounded-[var(--radius-md)] border border-amber-500/30 bg-amber-500/10 p-4">
|
||||
<p className="text-sm text-amber-200">
|
||||
This organization has no active subscription. Select a plan below to start
|
||||
the purchase process.
|
||||
</p>
|
||||
<p className="text-sm text-amber-200">{t('noSubscriptionNotice')}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-5">
|
||||
<div>
|
||||
<p className="text-xs text-text-muted uppercase tracking-wide">Current plan</p>
|
||||
<p className="text-xs text-text-muted uppercase tracking-wide">{t('currentPlan')}</p>
|
||||
<p className="text-lg font-medium text-text-primary capitalize">
|
||||
{plan?.name ?? '—'}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs text-text-muted uppercase tracking-wide">Plan price</p>
|
||||
<p className="text-xs text-text-muted uppercase tracking-wide">{t('planPrice')}</p>
|
||||
<p className="text-lg font-medium text-text-primary">
|
||||
{typeof plan?.price === 'number' ? `$${plan.price}` : '—'}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs text-text-muted uppercase tracking-wide">Seats used</p>
|
||||
<p className="text-xs text-text-muted uppercase tracking-wide">{t('seatsUsed')}</p>
|
||||
<p className="text-lg font-medium text-text-primary">
|
||||
{typeof seatsUsed === 'number' ? seatsUsed : '—'}
|
||||
{typeof maxUsers === 'number' ? ` / ${isUnlimited ? 'Unlimited' : maxUsers}` : ''}
|
||||
{typeof maxUsers === 'number'
|
||||
? ` / ${isUnlimited ? t('unlimited') : maxUsers}`
|
||||
: ''}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs text-text-muted uppercase tracking-wide">Seats remaining</p>
|
||||
<p className="text-xs text-text-muted uppercase tracking-wide">{t('seatsRemaining')}</p>
|
||||
<p className="text-lg font-medium text-text-primary">
|
||||
{isUnlimited ? 'Unlimited' : seatsRemaining ?? '—'}
|
||||
{isUnlimited ? t('unlimited') : seatsRemaining ?? '—'}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs text-text-muted uppercase tracking-wide">Days remaining</p>
|
||||
<p className="text-xs text-text-muted uppercase tracking-wide">{t('daysRemaining')}</p>
|
||||
<p className={`text-lg font-medium ${planDayTone}`}>
|
||||
{daysUntilPlanEnd ?? '—'}
|
||||
</p>
|
||||
@@ -133,27 +132,24 @@ export default function SubscriptionsSettingsPage() {
|
||||
{alert?.showWarning && (
|
||||
<div className="text-sm text-text-secondary space-y-1">
|
||||
{alert.noActiveSubscription && (
|
||||
<p>No active subscription for this organization.</p>
|
||||
<p>{t('noActiveSubscription')}</p>
|
||||
)}
|
||||
{alert.trialExpired && (
|
||||
<p>Trial period has ended. Choose a plan when checkout is available.</p>
|
||||
<p>{t('trialEnded')}</p>
|
||||
)}
|
||||
{!alert.trialExpired && alert.trialEndingSoon && (
|
||||
<p>
|
||||
Trial ends in {alert.daysUntilTrialEnd ?? '—'} day(s).
|
||||
{t('trialEndsIn', { days: alert.daysUntilTrialEnd ?? '—' })}
|
||||
</p>
|
||||
)}
|
||||
{!alert.trialExpired && !alert.trialEndingSoon && alert.seatsLow && (
|
||||
<p>Seat usage is high for this organization.</p>
|
||||
<p>{t('seatsLow')}</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="space-y-3 pt-2">
|
||||
<p className="text-sm text-text-secondary">
|
||||
Choose a plan to continue. Purchase integration is not active yet, so this
|
||||
currently prepares the selection step only.
|
||||
</p>
|
||||
<p className="text-sm text-text-secondary">{t('choosePlanIntro')}</p>
|
||||
<div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{PLAN_OPTIONS.map((option) => {
|
||||
const selected = selectedPlanId === option.id;
|
||||
@@ -168,11 +164,15 @@ export default function SubscriptionsSettingsPage() {
|
||||
: 'border-border hover:border-border-strong'
|
||||
}`}
|
||||
>
|
||||
<p className="text-base font-medium text-text-primary">{option.name}</p>
|
||||
<p className="text-base font-medium text-text-primary">{t(option.nameKey)}</p>
|
||||
<p className="text-sm text-text-secondary mt-1">
|
||||
{option.maxUsers == null ? 'Unlimited seats' : `${option.maxUsers} seats`}
|
||||
{option.maxUsers == null
|
||||
? t('unlimitedSeats')
|
||||
: t('seatsCount', { n: option.maxUsers })}
|
||||
</p>
|
||||
<p className="text-sm text-text-secondary mt-1">
|
||||
{t('pricePerMonth', { price: option.price })}
|
||||
</p>
|
||||
<p className="text-sm text-text-secondary mt-1">${option.price} / month</p>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
@@ -181,13 +181,13 @@ export default function SubscriptionsSettingsPage() {
|
||||
type="button"
|
||||
variant="primary"
|
||||
onClick={() => {
|
||||
const selectedPlanLabel = selectedPlan?.name ?? 'the selected plan';
|
||||
setPurchaseNotice(
|
||||
`Purchase flow will be enabled soon. ${selectedPlanLabel} is selected and ready for checkout setup.`,
|
||||
);
|
||||
const selectedPlanLabel = selectedPlan
|
||||
? t(selectedPlan.nameKey)
|
||||
: t('planSolo');
|
||||
setPurchaseNotice(t('purchaseNotice', { plan: selectedPlanLabel }));
|
||||
}}
|
||||
>
|
||||
Start purchase process
|
||||
{t('startPurchase')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user