feat: enhance dashboard and settings UI with new icons and improved layout
- Added new icons (CircleUserRoundIcon and LockKeyholeIcon) to the main navigation links for better visual representation. - Introduced a new data.json file to centralize dashboard data management, improving organization and modularity. - Updated the dashboard page to correctly import the new data source. - Enhanced the billing settings page layout with additional spacing and descriptions for better user guidance. - Improved mobile navbar styles for a more consistent user experience. - Refactored billing card component to streamline the display of subscription plans and billing history.
This commit is contained in:
parent
a9e0ce57d3
commit
db054ccdcc
@ -4,7 +4,7 @@ import { DataTable } from '@/components/dashboard/data-table';
|
||||
import { SectionCards } from '@/components/dashboard/section-cards';
|
||||
import { useTranslations } from 'next-intl';
|
||||
|
||||
import data from "../data.json";
|
||||
import data from "./data.json";
|
||||
|
||||
export default function DashboardPage() {
|
||||
const t = useTranslations();
|
||||
|
@ -1,8 +1,8 @@
|
||||
"use client";
|
||||
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { DashboardHeader } from '@/components/dashboard/dashboard-header';
|
||||
import BillingCard from '@/components/settings/billing/billing-card';
|
||||
import { useTranslations } from 'next-intl';
|
||||
|
||||
export default function SettingsBillingPage() {
|
||||
const t = useTranslations();
|
||||
@ -22,8 +22,19 @@ export default function SettingsBillingPage() {
|
||||
<>
|
||||
<DashboardHeader breadcrumbs={breadcrumbs} />
|
||||
|
||||
<div className="px-4 lg:px-6 py-8">
|
||||
<BillingCard />
|
||||
<div className="px-4 lg:px-6 py-16">
|
||||
<div className="max-w-5xl mx-auto space-y-10">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold tracking-tight">
|
||||
{t('Dashboard.sidebar.settings.items.billing.title')}
|
||||
</h1>
|
||||
<p className="text-muted-foreground mt-2">
|
||||
{t('Dashboard.sidebar.settings.items.billing.description')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<BillingCard />
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
@ -197,7 +197,7 @@ function MainMobileMenu({ userLoggedIn, onLinkClicked }: MainMobileMenuProps) {
|
||||
type="button"
|
||||
variant="ghost"
|
||||
className={cn(
|
||||
'flex w-full items-center justify-between text-left',
|
||||
'flex w-full !pl-0 items-center justify-between text-left',
|
||||
'bg-transparent text-muted-foreground cursor-pointer',
|
||||
'hover:bg-transparent hover:text-foreground',
|
||||
'focus:bg-transparent focus:text-foreground',
|
||||
@ -213,8 +213,8 @@ function MainMobileMenu({ userLoggedIn, onLinkClicked }: MainMobileMenuProps) {
|
||||
)}
|
||||
</Button>
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent>
|
||||
<ul className="mt-2 space-y-2">
|
||||
<CollapsibleContent className="pl-0">
|
||||
<ul className="mt-2 space-y-2 pl-0">
|
||||
{item.items.map((subItem) => {
|
||||
const isSubItemActive =
|
||||
subItem.href &&
|
||||
@ -234,7 +234,7 @@ function MainMobileMenu({ userLoggedIn, onLinkClicked }: MainMobileMenuProps) {
|
||||
}
|
||||
className={cn(
|
||||
buttonVariants({ variant: 'ghost' }),
|
||||
'group h-auto w-full justify-start gap-4 p-1 px-3',
|
||||
'group h-auto w-full justify-start gap-4 p-1 !pl-0 !pr-3',
|
||||
'bg-transparent text-muted-foreground cursor-pointer',
|
||||
'hover:bg-transparent hover:text-foreground',
|
||||
'focus:bg-transparent focus:text-foreground',
|
||||
@ -245,7 +245,7 @@ function MainMobileMenu({ userLoggedIn, onLinkClicked }: MainMobileMenuProps) {
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
'flex size-8 shrink-0 items-center justify-center transition-colors',
|
||||
'flex size-8 shrink-0 items-center justify-center transition-colors ml-0',
|
||||
'bg-transparent text-muted-foreground',
|
||||
'group-hover:bg-transparent group-hover:text-foreground',
|
||||
'group-focus:bg-transparent group-focus:text-foreground',
|
||||
@ -306,7 +306,7 @@ function MainMobileMenu({ userLoggedIn, onLinkClicked }: MainMobileMenuProps) {
|
||||
rel={item.external ? 'noopener noreferrer' : undefined}
|
||||
className={cn(
|
||||
buttonVariants({ variant: 'ghost' }),
|
||||
'w-full justify-start cursor-pointer',
|
||||
'w-full !pl-0 justify-start cursor-pointer group',
|
||||
'bg-transparent text-muted-foreground',
|
||||
'hover:bg-transparent hover:text-foreground',
|
||||
'focus:bg-transparent focus:text-foreground',
|
||||
@ -314,7 +314,9 @@ function MainMobileMenu({ userLoggedIn, onLinkClicked }: MainMobileMenuProps) {
|
||||
)}
|
||||
onClick={onLinkClicked}
|
||||
>
|
||||
<span className="text-base">{item.title}</span>
|
||||
<div className="flex items-center w-full pl-0">
|
||||
<span className="text-base">{item.title}</span>
|
||||
</div>
|
||||
</LocaleLink>
|
||||
)}
|
||||
</li>
|
||||
|
@ -2,6 +2,7 @@
|
||||
|
||||
import { createPortalAction } from '@/actions/payment';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { Loader2 } from 'lucide-react';
|
||||
import { useState } from 'react';
|
||||
|
||||
@ -57,7 +58,7 @@ export function CustomerPortalButton({
|
||||
<Button
|
||||
variant={variant}
|
||||
size={size}
|
||||
className={className}
|
||||
className={cn(className, 'cursor-pointer')}
|
||||
onClick={handleClick}
|
||||
disabled={isLoading}
|
||||
>
|
||||
|
@ -1,15 +1,15 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { CheckoutButton } from '@/components/payment/checkout-button';
|
||||
import { CustomerPortalButton } from '@/components/payment/customer-portal-button';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Skeleton } from '@/components/ui/skeleton';
|
||||
import { CustomerPortalButton } from '@/components/payment/customer-portal-button';
|
||||
import { CheckoutButton } from '@/components/payment/checkout-button';
|
||||
import { getAllPlans } from '@/payment';
|
||||
import { PricePlan } from '@/payment/types';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
// Utility function to format prices
|
||||
const formatPrice = (amount: number, currency: string = 'USD') => {
|
||||
@ -117,240 +117,134 @@ export default function BillingCard() {
|
||||
: null;
|
||||
|
||||
return (
|
||||
<div className="px-4 py-8">
|
||||
<div className="max-w-5xl mx-auto space-y-10">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold tracking-tight">
|
||||
{t('title')}
|
||||
</h1>
|
||||
<p className="text-muted-foreground mt-2">
|
||||
{t('description')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-8 md:grid-cols-2">
|
||||
{/* Current Plan Card */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>{t('currentPlan.title')}</CardTitle>
|
||||
<CardDescription>{t('currentPlan.description')}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
{loading ? (
|
||||
<div className="space-y-3">
|
||||
<Skeleton className="h-5 w-1/3" />
|
||||
<Skeleton className="h-4 w-1/2" />
|
||||
<Skeleton className="h-4 w-2/3" />
|
||||
<Skeleton className="h-4 w-1/4" />
|
||||
<div className="space-y-10">
|
||||
<div className="grid gap-8 md:grid-cols-2">
|
||||
{/* Current Plan Card */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>{t('currentPlan.title')}</CardTitle>
|
||||
<CardDescription>{t('currentPlan.description')}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
{loading ? (
|
||||
<div className="space-y-3">
|
||||
<Skeleton className="h-5 w-1/4" />
|
||||
<Skeleton className="h-6 w-full" />
|
||||
<Skeleton className="h-6 w-full" />
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="font-medium">{currentPlan?.name}</div>
|
||||
<Badge variant={currentPlan?.isFree ? 'outline' : 'default'}>
|
||||
{billingData.subscription?.status === 'active' ?
|
||||
t('status.active') :
|
||||
billingData.subscription?.status === 'trialing' ?
|
||||
t('status.trial') :
|
||||
t('status.free')}
|
||||
</Badge>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="font-medium">{currentPlan?.name}</div>
|
||||
<Badge variant={currentPlan?.isFree ? 'outline' : 'default'}>
|
||||
{billingData.subscription?.status === 'active' ?
|
||||
t('status.active') :
|
||||
billingData.subscription?.status === 'trialing' ?
|
||||
t('status.trial') :
|
||||
t('status.free')}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
{billingData.subscription && currentPrice && (
|
||||
<div className="text-sm text-muted-foreground space-y-1">
|
||||
<div>
|
||||
{formatPrice(currentPrice.amount, currentPrice.currency)} / {currentPrice.interval === 'month' ?
|
||||
t('interval.month') :
|
||||
currentPrice.interval === 'year' ?
|
||||
t('interval.year') :
|
||||
t('interval.oneTime')}
|
||||
{billingData.subscription && currentPrice && (
|
||||
<div className="text-sm text-muted-foreground space-y-1">
|
||||
<div>
|
||||
{formatPrice(currentPrice.amount, currentPrice.currency)} / {currentPrice.interval === 'month' ?
|
||||
t('interval.month') :
|
||||
currentPrice.interval === 'year' ?
|
||||
t('interval.year') :
|
||||
t('interval.oneTime')}
|
||||
</div>
|
||||
|
||||
{nextBillingDate && (
|
||||
<div>{t('nextBillingDate')} {nextBillingDate}</div>
|
||||
)}
|
||||
|
||||
{billingData.subscription.status === 'trialing' && (
|
||||
<div className="text-amber-500">
|
||||
{t('trialEnds')} {new Intl.DateTimeFormat('en-US', {
|
||||
day: 'numeric',
|
||||
month: 'long',
|
||||
year: 'numeric'
|
||||
}).format(billingData.subscription.currentPeriodEnd)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{currentPlan?.isFree && (
|
||||
<div className="text-sm text-muted-foreground">
|
||||
{t('freePlanMessage')}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</CardContent>
|
||||
<CardFooter>
|
||||
{loading ? (
|
||||
<Skeleton className="h-10 w-full" />
|
||||
) : billingData.subscription ? (
|
||||
<CustomerPortalButton
|
||||
customerId={billingData.user.customerId}
|
||||
className="w-full"
|
||||
>
|
||||
{t('manageSubscription')}
|
||||
</CustomerPortalButton>
|
||||
) : (
|
||||
<div className="text-sm text-muted-foreground">
|
||||
{t('upgradeMessage')}
|
||||
</div>
|
||||
)}
|
||||
</CardFooter>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Upgrade Options */}
|
||||
{!loading && !billingData.subscription && (
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold tracking-tight">{t('upgradePlan.title')}</h2>
|
||||
<p className="text-muted-foreground mt-1">
|
||||
{t('upgradePlan.description')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-4 md:grid-cols-2">
|
||||
{plans
|
||||
.filter(plan => !plan.isFree && !isEnterprisePlan(plan))
|
||||
.map(plan => {
|
||||
// Get monthly price if available, otherwise first price
|
||||
const price = plan.prices.find(p => p.type === 'recurring' && p.interval === 'month') || plan.prices[0];
|
||||
if (!price) return null;
|
||||
|
||||
return (
|
||||
<Card key={plan.id} className="flex flex-col">
|
||||
<CardHeader>
|
||||
<CardTitle>{plan.name}</CardTitle>
|
||||
<CardDescription>{plan.description}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="grow">
|
||||
<div className="mb-4">
|
||||
<span className="text-3xl font-bold">
|
||||
{formatPrice(price.amount, price.currency)}
|
||||
</span>
|
||||
<span className="text-muted-foreground">
|
||||
{price.interval === 'month' ?
|
||||
`/${t('interval.month')}` :
|
||||
price.interval === 'year' ?
|
||||
`/${t('interval.year')}` :
|
||||
''}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{nextBillingDate && (
|
||||
<div>{t('nextBillingDate')} {nextBillingDate}</div>
|
||||
)}
|
||||
|
||||
{billingData.subscription.status === 'trialing' && (
|
||||
<div className="text-amber-500">
|
||||
{t('trialEnds')} {new Intl.DateTimeFormat('en-US', {
|
||||
day: 'numeric',
|
||||
month: 'long',
|
||||
year: 'numeric'
|
||||
}).format(billingData.subscription.currentPeriodEnd)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{currentPlan?.isFree && (
|
||||
<div className="text-sm text-muted-foreground">
|
||||
{t('freePlanMessage')}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</CardContent>
|
||||
<CardFooter>
|
||||
{loading ? (
|
||||
<Skeleton className="h-10 w-full" />
|
||||
) : billingData.subscription ? (
|
||||
<CustomerPortalButton
|
||||
customerId={billingData.user.customerId}
|
||||
className="w-full"
|
||||
>
|
||||
{t('manageSubscription')}
|
||||
</CustomerPortalButton>
|
||||
) : (
|
||||
<div className="text-sm text-muted-foreground">
|
||||
{t('upgradeMessage')}
|
||||
</div>
|
||||
)}
|
||||
</CardFooter>
|
||||
</Card>
|
||||
|
||||
{/* Payment Method Card */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>{t('paymentMethod.title')}</CardTitle>
|
||||
<CardDescription>{t('paymentMethod.description')}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{loading ? (
|
||||
<div className="space-y-3">
|
||||
<Skeleton className="h-5 w-1/2" />
|
||||
<Skeleton className="h-4 w-3/4" />
|
||||
<Skeleton className="h-4 w-1/2" />
|
||||
</div>
|
||||
) : billingData.subscription ? (
|
||||
<div className="text-sm">
|
||||
<p>{t('paymentMethod.manageMessage')}</p>
|
||||
<p className="mt-2 text-muted-foreground">
|
||||
{t('paymentMethod.securityMessage')}
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-sm">
|
||||
<p>{t('paymentMethod.noMethodsMessage')}</p>
|
||||
<p className="mt-2 text-muted-foreground">
|
||||
{t('paymentMethod.upgradePromptMessage')}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
<CardFooter>
|
||||
{loading ? (
|
||||
<Skeleton className="h-10 w-full" />
|
||||
) : billingData.subscription ? (
|
||||
<CustomerPortalButton
|
||||
customerId={billingData.user.customerId}
|
||||
className="w-full cursor-pointer"
|
||||
>
|
||||
{t('managePaymentMethods')}
|
||||
</CustomerPortalButton>
|
||||
) : (
|
||||
<div />
|
||||
)}
|
||||
</CardFooter>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Upgrade Options */}
|
||||
{!loading && !billingData.subscription && (
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold tracking-tight">{t('upgradePlan.title')}</h2>
|
||||
<p className="text-muted-foreground mt-1">
|
||||
{t('upgradePlan.description')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-4 md:grid-cols-2">
|
||||
{plans
|
||||
.filter(plan => !plan.isFree && !isEnterprisePlan(plan))
|
||||
.map(plan => {
|
||||
// Get monthly price if available, otherwise first price
|
||||
const price = plan.prices.find(p => p.type === 'recurring' && p.interval === 'month') || plan.prices[0];
|
||||
if (!price) return null;
|
||||
|
||||
return (
|
||||
<Card key={plan.id} className="flex flex-col">
|
||||
<CardHeader>
|
||||
<CardTitle>{plan.name}</CardTitle>
|
||||
<CardDescription>{plan.description}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="grow">
|
||||
<div className="mb-4">
|
||||
<span className="text-3xl font-bold">
|
||||
{formatPrice(price.amount, price.currency)}
|
||||
</span>
|
||||
<span className="text-muted-foreground">
|
||||
{price.interval === 'month' ?
|
||||
`/${t('interval.month')}` :
|
||||
price.interval === 'year' ?
|
||||
`/${t('interval.year')}` :
|
||||
''}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{price.trialPeriodDays && price.trialPeriodDays > 0 && (
|
||||
<Badge variant="outline" className="mb-4">
|
||||
{t('trialDays', {
|
||||
days: price.trialPeriodDays
|
||||
})}
|
||||
</Badge>
|
||||
)}
|
||||
|
||||
<ul className="space-y-2 mb-6">
|
||||
{plan.features.map((feature, index) => (
|
||||
<li key={index} className="flex items-start">
|
||||
<svg
|
||||
className="h-5 w-5 text-primary shrink-0 mr-2"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M5 13l4 4L19 7"
|
||||
/>
|
||||
</svg>
|
||||
<span className="text-sm">{feature}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</CardContent>
|
||||
<CardFooter>
|
||||
<CheckoutButton
|
||||
planId={plan.id}
|
||||
priceId={price.productId}
|
||||
email={billingData.user.email}
|
||||
metadata={{ userId: billingData.user.id }}
|
||||
className="w-full"
|
||||
>
|
||||
{t('upgradeToPlan', {
|
||||
planName: plan.name
|
||||
{price.trialPeriodDays && price.trialPeriodDays > 0 && (
|
||||
<Badge variant="outline" className="mb-4">
|
||||
{t('trialDays', {
|
||||
days: price.trialPeriodDays
|
||||
})}
|
||||
</CheckoutButton>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</Badge>
|
||||
)}
|
||||
|
||||
{/* Enterprise Plan */}
|
||||
{plans
|
||||
.filter(plan => isEnterprisePlan(plan))
|
||||
.map(plan => (
|
||||
<Card key={plan.id} className="bg-muted/40">
|
||||
<CardHeader>
|
||||
<CardTitle>{plan.name}</CardTitle>
|
||||
<CardDescription>{plan.description}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="flex flex-col md:flex-row md:items-center md:justify-between">
|
||||
<ul className="space-y-2 mb-6 md:mb-0">
|
||||
<ul className="space-y-2 mb-6">
|
||||
{plan.features.map((feature, index) => (
|
||||
<li key={index} className="flex items-start">
|
||||
<svg
|
||||
@ -370,59 +264,108 @@ export default function BillingCard() {
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</CardContent>
|
||||
<CardFooter>
|
||||
<CheckoutButton
|
||||
planId={plan.id}
|
||||
priceId={price.productId}
|
||||
email={billingData.user.email}
|
||||
metadata={{ userId: billingData.user.id }}
|
||||
className="w-full"
|
||||
>
|
||||
{t('upgradeToPlan', {
|
||||
planName: plan.name
|
||||
})}
|
||||
</CheckoutButton>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col items-start md:items-end">
|
||||
<span className="text-xl font-bold mb-2">{t('customPricing')}</span>
|
||||
<Button className="w-full md:w-auto" variant="default" asChild>
|
||||
<a href="mailto:sales@yourcompany.com">{t('contactSales')}</a>
|
||||
</Button>
|
||||
</div>
|
||||
{/* Enterprise Plan */}
|
||||
{plans
|
||||
.filter(plan => isEnterprisePlan(plan))
|
||||
.map(plan => (
|
||||
<Card key={plan.id} className="bg-muted/40">
|
||||
<CardHeader>
|
||||
<CardTitle>{plan.name}</CardTitle>
|
||||
<CardDescription>{plan.description}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="flex flex-col md:flex-row md:items-center md:justify-between">
|
||||
<ul className="space-y-2 mb-6 md:mb-0">
|
||||
{plan.features.map((feature, index) => (
|
||||
<li key={index} className="flex items-start">
|
||||
<svg
|
||||
className="h-5 w-5 text-primary shrink-0 mr-2"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M5 13l4 4L19 7"
|
||||
/>
|
||||
</svg>
|
||||
<span className="text-sm">{feature}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
<div className="flex flex-col items-start md:items-end">
|
||||
<span className="text-xl font-bold mb-2">{t('customPricing')}</span>
|
||||
<Button className="w-full md:w-auto" variant="default" asChild>
|
||||
<a href="mailto:sales@yourcompany.com">{t('contactSales')}</a>
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Billing History */}
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold tracking-tight">{t('billingHistory.title')}</h2>
|
||||
<p className="text-muted-foreground mt-1">
|
||||
{t('billingHistory.description')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Card>
|
||||
<CardContent className="pt-6">
|
||||
{loading ? (
|
||||
<div className="space-y-3">
|
||||
<Skeleton className="h-5 w-full" />
|
||||
<Skeleton className="h-5 w-full" />
|
||||
<Skeleton className="h-5 w-full" />
|
||||
</div>
|
||||
) : billingData.subscription ? (
|
||||
<div className="text-center py-4">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{t('billingHistory.accessMessage')}
|
||||
</p>
|
||||
<CustomerPortalButton
|
||||
customerId={billingData.user.customerId}
|
||||
className="mt-4"
|
||||
>
|
||||
{t('viewBillingHistory')}
|
||||
</CustomerPortalButton>
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-center py-4">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{t('billingHistory.noHistoryMessage')}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Billing History */}
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold tracking-tight">{t('billingHistory.title')}</h2>
|
||||
<p className="text-muted-foreground mt-1">
|
||||
{t('billingHistory.description')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Card>
|
||||
<CardContent className="pt-6">
|
||||
{loading ? (
|
||||
<div className="space-y-3">
|
||||
<Skeleton className="h-5 w-full" />
|
||||
<Skeleton className="h-5 w-full" />
|
||||
<Skeleton className="h-5 w-full" />
|
||||
</div>
|
||||
) : billingData.subscription ? (
|
||||
<div className="text-center py-4">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{t('billingHistory.accessMessage')}
|
||||
</p>
|
||||
<CustomerPortalButton
|
||||
customerId={billingData.user.customerId}
|
||||
className="mt-4"
|
||||
>
|
||||
{t('viewBillingHistory')}
|
||||
</CustomerPortalButton>
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-center py-4">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{t('billingHistory.noHistoryMessage')}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
@ -17,6 +17,7 @@ interface ConditionalUpdatePasswordCardProps {
|
||||
* Conditionally renders either:
|
||||
* - UpdatePasswordCard: if the user has a credential provider (email/password login)
|
||||
* - ResetPasswordCard: if the user only has social login providers and has an email
|
||||
* - PasswordSkeletonCard: when this component is still loading
|
||||
* - Nothing: if the user has no credential provider and no email
|
||||
*/
|
||||
export function ConditionalUpdatePasswordCard({ className }: ConditionalUpdatePasswordCardProps) {
|
||||
|
@ -26,7 +26,7 @@ interface ResetPasswordCardProps {
|
||||
*
|
||||
* How it works:
|
||||
* 1. When a user signs in with a social provider, they don't have a password set up
|
||||
* 2. This card provides a way for them to set up a password using the forgot password flow
|
||||
* 2. This component provides a way for them to set up a password using the forgot password flow
|
||||
* 3. The user clicks the button and is redirected to the forgot password page
|
||||
* 4. They enter their email (which is already associated with their account)
|
||||
* 5. They receive a password reset email
|
||||
|
@ -39,6 +39,7 @@ interface UpdatePasswordCardProps {
|
||||
* Update user password
|
||||
*
|
||||
* This component allows users to update their password.
|
||||
*
|
||||
* NOTE: This should only be used for users with credential providers (email/password login).
|
||||
* For conditional rendering based on provider type, use ConditionalUpdatePasswordCard instead.
|
||||
*
|
||||
|
@ -16,6 +16,7 @@ import {
|
||||
ChartNoAxesCombinedIcon,
|
||||
CircleDollarSignIcon,
|
||||
CircleHelpIcon,
|
||||
CircleUserRoundIcon,
|
||||
CookieIcon,
|
||||
CreditCardIcon,
|
||||
FileTextIcon,
|
||||
@ -24,6 +25,7 @@ import {
|
||||
ImageIcon,
|
||||
LayoutDashboardIcon,
|
||||
ListChecksIcon,
|
||||
LockKeyholeIcon,
|
||||
MailboxIcon,
|
||||
MailIcon,
|
||||
NewspaperIcon,
|
||||
@ -33,7 +35,6 @@ import {
|
||||
SquareKanbanIcon,
|
||||
SquarePenIcon,
|
||||
ThumbsUpIcon,
|
||||
UserCircleIcon,
|
||||
WandSparklesIcon
|
||||
} from 'lucide-react';
|
||||
import { useTranslations } from 'next-intl';
|
||||
@ -418,7 +419,7 @@ export function getNavMainLinks(): NestedMenuItem[] {
|
||||
items: [
|
||||
{
|
||||
title: t('Dashboard.sidebar.settings.items.profile.title'),
|
||||
icon: <UserCircleIcon className="site-4 shrink-0" />,
|
||||
icon: <CircleUserRoundIcon className="site-4 shrink-0" />,
|
||||
href: Routes.SettingsProfile,
|
||||
external: false,
|
||||
},
|
||||
@ -430,7 +431,7 @@ export function getNavMainLinks(): NestedMenuItem[] {
|
||||
},
|
||||
{
|
||||
title: t('Dashboard.sidebar.settings.items.security.title'),
|
||||
icon: <ShieldCheckIcon className="site-4 shrink-0" />,
|
||||
icon: <LockKeyholeIcon className="site-4 shrink-0" />,
|
||||
href: Routes.SettingsSecurity,
|
||||
external: false,
|
||||
},
|
||||
|
Loading…
Reference in New Issue
Block a user