diff --git a/apps/web/app/(marketing)/_components/site-page-header.tsx b/apps/web/app/(marketing)/_components/site-page-header.tsx index b22e05a18..8445ba928 100644 --- a/apps/web/app/(marketing)/_components/site-page-header.tsx +++ b/apps/web/app/(marketing)/_components/site-page-header.tsx @@ -1,19 +1,27 @@ import { cn } from '@kit/ui/utils'; -export function SitePageHeader(props: { +export function SitePageHeader({ + title, + subtitle, + container = true, + className = '' +}: { title: string; subtitle: string; + container?: boolean; className?: string; }) { + const containerClass = container ? 'container' : ''; + return ( -
-
+
+

- {props.title} + {title}

- {props.subtitle} + {subtitle}

diff --git a/apps/web/app/(marketing)/blog/_components/html-renderer.module.css b/apps/web/app/(marketing)/blog/_components/html-renderer.module.css index 65d0b7099..605860041 100644 --- a/apps/web/app/(marketing)/blog/_components/html-renderer.module.css +++ b/apps/web/app/(marketing)/blog/_components/html-renderer.module.css @@ -37,11 +37,11 @@ For more info: https://github.com/tailwindlabs/tailwindcss/issues/3258#issuecomm } .HTML p { - @apply mb-4 mt-2 text-base leading-7; + @apply mb-4 mt-2 text-base leading-7 text-muted-foreground; } .HTML li { - @apply relative my-1.5 text-base leading-7; + @apply relative my-1.5 text-base leading-7 text-muted-foreground; } .HTML ul > li:before { diff --git a/apps/web/app/(marketing)/docs/[...slug]/page.tsx b/apps/web/app/(marketing)/docs/[...slug]/page.tsx index b07fd4c4c..52b51ac5f 100644 --- a/apps/web/app/(marketing)/docs/[...slug]/page.tsx +++ b/apps/web/app/(marketing)/docs/[...slug]/page.tsx @@ -52,9 +52,14 @@ async function DocumentationPage({ params }: PageParams) { return (
- + -
+
diff --git a/apps/web/app/(marketing)/docs/page.tsx b/apps/web/app/(marketing)/docs/page.tsx index 9f1767e44..f8ded523a 100644 --- a/apps/web/app/(marketing)/docs/page.tsx +++ b/apps/web/app/(marketing)/docs/page.tsx @@ -1,5 +1,3 @@ -import { PageBody } from '@kit/ui/page'; - import { createI18nServerInstance } from '~/lib/i18n/i18n.server'; import { withI18n } from '~/lib/i18n/with-i18n'; @@ -23,20 +21,18 @@ async function DocsPage() { const cards = items.filter((item) => !item.parentId); return ( - -
- +
+ -
-
- -
+
+
+
- +
); } diff --git a/apps/web/app/auth/password-reset/page.tsx b/apps/web/app/auth/password-reset/page.tsx index e6801dc15..48ee7ac6e 100644 --- a/apps/web/app/auth/password-reset/page.tsx +++ b/apps/web/app/auth/password-reset/page.tsx @@ -23,7 +23,7 @@ const redirectPath = `${callback}?next=${passwordUpdate}`; function PasswordResetPage() { return ( <> - + diff --git a/apps/web/app/auth/sign-in/page.tsx b/apps/web/app/auth/sign-in/page.tsx index f07269dbf..1aed8ecf2 100644 --- a/apps/web/app/auth/sign-in/page.tsx +++ b/apps/web/app/auth/sign-in/page.tsx @@ -39,7 +39,7 @@ function SignInPage({ searchParams }: SignInPageProps) { return ( <> - + diff --git a/apps/web/app/auth/sign-up/page.tsx b/apps/web/app/auth/sign-up/page.tsx index 2375e4e66..5b3c2aa73 100644 --- a/apps/web/app/auth/sign-up/page.tsx +++ b/apps/web/app/auth/sign-up/page.tsx @@ -38,7 +38,7 @@ function SignUpPage({ searchParams }: Props) { return ( <> - + diff --git a/apps/web/app/home/(user)/_components/home-accounts-list.tsx b/apps/web/app/home/(user)/_components/home-accounts-list.tsx index 5e8f006d5..3d6c36302 100644 --- a/apps/web/app/home/(user)/_components/home-accounts-list.tsx +++ b/apps/web/app/home/(user)/_components/home-accounts-list.tsx @@ -7,7 +7,12 @@ import { CardButtonHeader, CardButtonTitle, } from '@kit/ui/card-button'; -import { Heading } from '@kit/ui/heading'; +import { + EmptyState, + EmptyStateButton, + EmptyStateHeading, + EmptyStateText, +} from '@kit/ui/empty-state'; import { Trans } from '@kit/ui/trans'; import { loadUserWorkspace } from '../_lib/server/load-user-workspace'; @@ -39,21 +44,18 @@ export function HomeAccountsList() { function HomeAccountsListEmptyState() { return ( -
-
- +
+ + + + + - - - + + - -
- - + +
); } diff --git a/apps/web/app/home/(user)/_components/home-add-account-button.tsx b/apps/web/app/home/(user)/_components/home-add-account-button.tsx index f6d33cfeb..4a2cabba8 100644 --- a/apps/web/app/home/(user)/_components/home-add-account-button.tsx +++ b/apps/web/app/home/(user)/_components/home-add-account-button.tsx @@ -6,12 +6,15 @@ import { CreateTeamAccountDialog } from '@kit/team-accounts/components'; import { Button } from '@kit/ui/button'; import { Trans } from '@kit/ui/trans'; -export function HomeAddAccountButton() { +export function HomeAddAccountButton(props: { className?: string }) { const [isAddingAccount, setIsAddingAccount] = useState(false); return ( <> - diff --git a/apps/web/app/home/(user)/billing/page.tsx b/apps/web/app/home/(user)/billing/page.tsx index 6ea32ef5f..de27213dc 100644 --- a/apps/web/app/home/(user)/billing/page.tsx +++ b/apps/web/app/home/(user)/billing/page.tsx @@ -7,6 +7,7 @@ import { If } from '@kit/ui/if'; import { PageBody } from '@kit/ui/page'; import { Trans } from '@kit/ui/trans'; +import { AppBreadcrumbs } from '@kit/ui/app-breadcrumbs'; import billingConfig from '~/config/billing.config'; import { createI18nServerInstance } from '~/lib/i18n/i18n.server'; import { withI18n } from '~/lib/i18n/with-i18n'; @@ -35,8 +36,8 @@ async function PersonalAccountBillingPage() { return ( <> } - description={} + title={} + description={} /> diff --git a/apps/web/app/home/(user)/page.tsx b/apps/web/app/home/(user)/page.tsx index 843362c21..3327e1f2f 100644 --- a/apps/web/app/home/(user)/page.tsx +++ b/apps/web/app/home/(user)/page.tsx @@ -20,7 +20,7 @@ function UserHomePage() { return ( <> } + title={} description={} /> diff --git a/apps/web/app/home/(user)/settings/layout.tsx b/apps/web/app/home/(user)/settings/layout.tsx index 476a62d28..ab68a7caf 100644 --- a/apps/web/app/home/(user)/settings/layout.tsx +++ b/apps/web/app/home/(user)/settings/layout.tsx @@ -1,5 +1,6 @@ import { Trans } from '@kit/ui/trans'; +import { AppBreadcrumbs } from '@kit/ui/app-breadcrumbs'; import { withI18n } from '~/lib/i18n/with-i18n'; // local imports @@ -9,8 +10,8 @@ function UserSettingsLayout(props: React.PropsWithChildren) { return ( <> } - description={} + title={} + description={} /> {props.children} diff --git a/apps/web/app/home/[account]/_components/dashboard-demo.tsx b/apps/web/app/home/[account]/_components/dashboard-demo.tsx index 4afbc2aba..41145a7cd 100644 --- a/apps/web/app/home/[account]/_components/dashboard-demo.tsx +++ b/apps/web/app/home/[account]/_components/dashboard-demo.tsx @@ -1,12 +1,34 @@ 'use client'; -import { useMemo } from 'react'; +import { useMemo, useState } from 'react'; -import { ArrowDown, ArrowUp, Menu } from 'lucide-react'; -import { Line, LineChart, ResponsiveContainer, XAxis } from 'recharts'; +import { ArrowDown, ArrowUp, Menu, TrendingUp } from 'lucide-react'; +import { + Area, + AreaChart, + Bar, + BarChart, + CartesianGrid, + Line, + LineChart, + XAxis, +} from 'recharts'; import { Badge } from '@kit/ui/badge'; -import { Card, CardContent, CardHeader, CardTitle } from '@kit/ui/card'; +import { + Card, + CardContent, + CardDescription, + CardFooter, + CardHeader, + CardTitle, +} from '@kit/ui/card'; +import { + ChartConfig, + ChartContainer, + ChartTooltip, + ChartTooltipContent, +} from '@kit/ui/chart'; import { Table, TableBody, @@ -18,183 +40,115 @@ import { export default function DashboardDemo() { const mrr = useMemo(() => generateDemoData(), []); - const visitors = useMemo(() => generateDemoData(), []); - const returningVisitors = useMemo(() => generateDemoData(), []); - const churn = useMemo(() => generateDemoData(), []); const netRevenue = useMemo(() => generateDemoData(), []); const fees = useMemo(() => generateDemoData(), []); const newCustomers = useMemo(() => generateDemoData(), []); - const tickets = useMemo(() => generateDemoData(), []); - const activeUsers = useMemo(() => generateDemoData(), []); return ( -
+
- - Monthly Recurring Revenue + + MRR 20% - - -
+ + Monthly recurring revenue + + +
{`$${mrr[1]}`}
+ + - + Revenue 12% + + + Total revenue including fees + + +
+
{`$${netRevenue[1]}`}
+
-
-
{`$${netRevenue[1]}`}
-
-
- + Fees 9% + + + Total fees collected + + +
+
{`$${fees[1]}`}
+
-
-
{`$${fees[1]}`}
-
-
- + New Customers -25% + + + Customers who signed up this month + + +
+
{`${Number(newCustomers[1]).toFixed(0)}`}
+
-
-
{`${newCustomers[1]}`}
-
-
- - - - - Visitors - -4.3% - - - - -
-
{visitors[1]}
-
- - -
-
- - - - - Returning Visitors - 10% - - - - -
-
{returningVisitors[1]}
-
- - -
-
- - - - - Churn - -10% - - - - -
-
{churn[1]}%
-
- - -
-
- - - - - Support Tickets - -30% - - - - -
-
{tickets[1]}
-
- - -
-
+ + + +
- - Active Users - 10% - - - - -
-
{activeUsers[1]}
-
- - -
-
-
- -
- - - Customers + Best Customers + Showing the top customers by MRR @@ -232,48 +186,227 @@ function generateDemoData() { function Chart( props: React.PropsWithChildren<{ data: { value: string; name: string }[] }>, ) { - return ( -
- - - + const chartConfig = { + desktop: { + label: 'Desktop', + color: 'hsl(var(--chart-1))', + }, + mobile: { + label: 'Mobile', + color: 'hsl(var(--chart-2))', + }, + } satisfies ChartConfig; - - - -
+ return ( + + + + + } + /> + + + ); } function CustomersTable() { + const customers = [ + { + name: 'John Doe', + email: 'john@makerkit.dev', + plan: 'Pro', + mrr: '$120.5', + logins: 1020, + status: 'Healthy', + trend: 'up', + }, + { + name: 'Emma Smith', + email: 'emma@makerit.dev', + plan: 'Basic', + mrr: '$65.4', + logins: 570, + status: 'Possible Churn', + trend: 'stale', + }, + { + name: 'Robert Johnson', + email: 'robert@makerkit.dev', + plan: 'Pro', + mrr: '$500.1', + logins: 2050, + status: 'Healthy', + trend: 'up', + }, + { + name: 'Olivia Brown', + email: 'olivia@makerkit.dev', + plan: 'Basic', + mrr: '$10', + logins: 50, + status: 'Churn', + trend: 'down', + }, + { + name: 'Michael Davis', + email: 'michael@makerkit.dev', + plan: 'Pro', + mrr: '$300.2', + logins: 1520, + status: 'Healthy', + trend: 'up', + }, + { + name: 'Emily Jones', + email: 'emily@makerkit.dev', + plan: 'Pro', + mrr: '$75.7', + logins: 780, + status: 'Healthy', + trend: 'up', + }, + { + name: 'Daniel Garcia', + email: 'daniel@makerkit.dev', + plan: 'Basic', + mrr: '$50', + logins: 320, + status: 'Possible Churn', + trend: 'stale', + }, + { + name: 'Liam Miller', + email: 'liam@makerkit.dev', + plan: 'Pro', + mrr: '$90.8', + logins: 1260, + status: 'Healthy', + trend: 'up', + }, + { + name: 'Emma Clark', + email: 'emma@makerkit.dev', + plan: 'Basic', + mrr: '$0', + logins: 20, + status: 'Churn', + trend: 'down', + }, + { + name: 'Elizabeth Rodriguez', + email: 'liz@makerkit.dev', + plan: 'Pro', + mrr: '$145.3', + logins: 1380, + status: 'Healthy', + trend: 'up', + }, + { + name: 'James Martinez', + email: 'james@makerkit.dev', + plan: 'Pro', + mrr: '$120.5', + logins: 940, + status: 'Healthy', + trend: 'up', + }, + { + name: 'Charlotte Ryan', + email: 'carlotte@makerkit.dev', + plan: 'Basic', + mrr: '$80.6', + logins: 460, + status: 'Possible Churn', + trend: 'stale', + }, + { + name: 'Lucas Evans', + email: 'lucas@makerkit.dev', + plan: 'Pro', + mrr: '$210.3', + logins: 1850, + status: 'Healthy', + trend: 'up', + }, + { + name: 'Sophia Wilson', + email: 'sophia@makerkit.dev', + plan: 'Basic', + mrr: '$10', + logins: 35, + status: 'Churn', + trend: 'down', + }, + { + name: 'William Kelly', + email: 'will@makerkit.dev', + plan: 'Pro', + mrr: '$350.2', + logins: 1760, + status: 'Healthy', + trend: 'up', + }, + { + name: 'Oliver Thomas', + email: 'olly@makerkit.dev', + plan: 'Pro', + mrr: '$145.6', + logins: 1350, + status: 'Healthy', + trend: 'up', + }, + { + name: 'Samantha White', + email: 'sam@makerkit.dev', + plan: 'Basic', + mrr: '$60.3', + logins: 425, + status: 'Possible Churn', + trend: 'stale', + }, + { + name: 'Benjamin Lewis', + email: 'ben@makerkit.dev', + plan: 'Pro', + mrr: '$175.8', + logins: 1600, + status: 'Healthy', + trend: 'up', + }, + { + name: 'Zoe Harris', + email: 'zoe@makerkit.dev', + plan: 'Basic', + mrr: '$0', + logins: 18, + status: 'Churn', + trend: 'down', + }, + { + name: 'Zachary Nelson', + email: 'zac@makerkit.dev', + plan: 'Pro', + mrr: '$255.9', + logins: 1785, + status: 'Healthy', + trend: 'up', + }, + ]; + return ( @@ -285,47 +418,25 @@ function CustomersTable() { Status - - - Pippin Oddo - Pro - $100.2 - 920 - - Healthy - - - - - Väinö Pánfilo - Basic - $40.6 - 300 - - Possible Churn - - - - - Giorgos Quinten - Pro - $2004.3 - 1000 - - Healthy - - - - - Adhelm Otis - Basic - $0 - 10 - - Churned - - + {customers.map((customer) => ( + + + {customer.name} + + {customer.email} + + + {customer.plan} + {customer.mrr} + {customer.logins} + + + {customer.status} + + + + ))}
); @@ -344,7 +455,10 @@ function BadgeWithTrend(props: React.PropsWithChildren<{ trend: string }>) { }, [props.trend]); return ( - + {props.children} ); @@ -352,7 +466,7 @@ function BadgeWithTrend(props: React.PropsWithChildren<{ trend: string }>) { function Figure(props: React.PropsWithChildren) { return ( -
+
{props.children}
); @@ -366,18 +480,18 @@ function Trend( const Icon = useMemo(() => { switch (props.trend) { case 'up': - return ; + return ; case 'down': - return ; + return ; case 'stale': - return ; + return ; } }, [props.trend]); return (
- + {Icon} {props.children} @@ -385,3 +499,395 @@ function Trend(
); } + +export function VisitorsChart() { + const chartData = useMemo(() => [ + { date: '2024-04-01', desktop: 222, mobile: 150 }, + { date: '2024-04-02', desktop: 97, mobile: 180 }, + { date: '2024-04-03', desktop: 167, mobile: 120 }, + { date: '2024-04-04', desktop: 242, mobile: 260 }, + { date: '2024-04-05', desktop: 373, mobile: 290 }, + { date: '2024-04-06', desktop: 301, mobile: 340 }, + { date: '2024-04-07', desktop: 245, mobile: 180 }, + { date: '2024-04-08', desktop: 409, mobile: 320 }, + { date: '2024-04-09', desktop: 59, mobile: 110 }, + { date: '2024-04-10', desktop: 261, mobile: 190 }, + { date: '2024-04-11', desktop: 327, mobile: 350 }, + { date: '2024-04-12', desktop: 292, mobile: 210 }, + { date: '2024-04-13', desktop: 342, mobile: 380 }, + { date: '2024-04-14', desktop: 137, mobile: 220 }, + { date: '2024-04-15', desktop: 120, mobile: 170 }, + { date: '2024-04-16', desktop: 138, mobile: 190 }, + { date: '2024-04-17', desktop: 446, mobile: 360 }, + { date: '2024-04-18', desktop: 364, mobile: 410 }, + { date: '2024-04-19', desktop: 243, mobile: 180 }, + { date: '2024-04-20', desktop: 89, mobile: 150 }, + { date: '2024-04-21', desktop: 137, mobile: 200 }, + { date: '2024-04-22', desktop: 224, mobile: 170 }, + { date: '2024-04-23', desktop: 138, mobile: 230 }, + { date: '2024-04-24', desktop: 387, mobile: 290 }, + { date: '2024-04-25', desktop: 215, mobile: 250 }, + { date: '2024-04-26', desktop: 75, mobile: 130 }, + { date: '2024-04-27', desktop: 383, mobile: 420 }, + { date: '2024-04-28', desktop: 122, mobile: 180 }, + { date: '2024-04-29', desktop: 315, mobile: 240 }, + { date: '2024-04-30', desktop: 454, mobile: 380 }, + { date: '2024-05-01', desktop: 165, mobile: 220 }, + { date: '2024-05-02', desktop: 293, mobile: 310 }, + { date: '2024-05-03', desktop: 247, mobile: 190 }, + { date: '2024-05-04', desktop: 385, mobile: 420 }, + { date: '2024-05-05', desktop: 481, mobile: 390 }, + { date: '2024-05-06', desktop: 498, mobile: 520 }, + { date: '2024-05-07', desktop: 388, mobile: 300 }, + { date: '2024-05-08', desktop: 149, mobile: 210 }, + { date: '2024-05-09', desktop: 227, mobile: 180 }, + { date: '2024-05-10', desktop: 293, mobile: 330 }, + { date: '2024-05-11', desktop: 335, mobile: 270 }, + { date: '2024-05-12', desktop: 197, mobile: 240 }, + { date: '2024-05-13', desktop: 197, mobile: 160 }, + { date: '2024-05-14', desktop: 448, mobile: 490 }, + { date: '2024-05-15', desktop: 473, mobile: 380 }, + { date: '2024-05-16', desktop: 338, mobile: 400 }, + { date: '2024-05-17', desktop: 499, mobile: 420 }, + { date: '2024-05-18', desktop: 315, mobile: 350 }, + { date: '2024-05-19', desktop: 235, mobile: 180 }, + { date: '2024-05-20', desktop: 177, mobile: 230 }, + { date: '2024-05-21', desktop: 82, mobile: 140 }, + { date: '2024-05-22', desktop: 81, mobile: 120 }, + { date: '2024-05-23', desktop: 252, mobile: 290 }, + { date: '2024-05-24', desktop: 294, mobile: 220 }, + { date: '2024-05-25', desktop: 201, mobile: 250 }, + { date: '2024-05-26', desktop: 213, mobile: 170 }, + { date: '2024-05-27', desktop: 420, mobile: 460 }, + { date: '2024-05-28', desktop: 233, mobile: 190 }, + { date: '2024-05-29', desktop: 78, mobile: 130 }, + { date: '2024-05-30', desktop: 340, mobile: 280 }, + { date: '2024-05-31', desktop: 178, mobile: 230 }, + { date: '2024-06-01', desktop: 178, mobile: 200 }, + { date: '2024-06-02', desktop: 470, mobile: 410 }, + { date: '2024-06-03', desktop: 103, mobile: 160 }, + { date: '2024-06-04', desktop: 439, mobile: 380 }, + { date: '2024-06-05', desktop: 88, mobile: 140 }, + { date: '2024-06-06', desktop: 294, mobile: 250 }, + { date: '2024-06-07', desktop: 323, mobile: 370 }, + { date: '2024-06-08', desktop: 385, mobile: 320 }, + { date: '2024-06-09', desktop: 438, mobile: 480 }, + { date: '2024-06-10', desktop: 155, mobile: 200 }, + { date: '2024-06-11', desktop: 92, mobile: 150 }, + { date: '2024-06-12', desktop: 492, mobile: 420 }, + { date: '2024-06-13', desktop: 81, mobile: 130 }, + { date: '2024-06-14', desktop: 426, mobile: 380 }, + { date: '2024-06-15', desktop: 307, mobile: 350 }, + { date: '2024-06-16', desktop: 371, mobile: 310 }, + { date: '2024-06-17', desktop: 475, mobile: 520 }, + { date: '2024-06-18', desktop: 107, mobile: 170 }, + { date: '2024-06-19', desktop: 341, mobile: 290 }, + { date: '2024-06-20', desktop: 408, mobile: 450 }, + { date: '2024-06-21', desktop: 169, mobile: 210 }, + { date: '2024-06-22', desktop: 317, mobile: 270 }, + { date: '2024-06-23', desktop: 480, mobile: 530 }, + { date: '2024-06-24', desktop: 132, mobile: 180 }, + { date: '2024-06-25', desktop: 141, mobile: 190 }, + { date: '2024-06-26', desktop: 434, mobile: 380 }, + { date: '2024-06-27', desktop: 448, mobile: 490 }, + { date: '2024-06-28', desktop: 149, mobile: 200 }, + { date: '2024-06-29', desktop: 103, mobile: 160 }, + { date: '2024-06-30', desktop: 446, mobile: 400 }, + ], []); + + const chartConfig = { + visitors: { + label: 'Visitors', + }, + desktop: { + label: 'Desktop', + color: 'hsl(var(--chart-1))', + }, + mobile: { + label: 'Mobile', + color: 'hsl(var(--chart-2))', + }, + } satisfies ChartConfig; + + return ( + + + Visitors + + Showing total visitors for the last 6 months + + + + + + + + + + + + + + + + + + value.slice(0, 3)} + /> + } + /> + + + + + + + +
+
+
+ Trending up by 5.2% this month +
+
+ January - June 2024 +
+
+
+
+
+ ); +} + +export function PageViewsChart() { + const [activeChart, setActiveChart] = + useState('desktop'); + + const chartData = [ + { date: '2024-04-01', desktop: 222, mobile: 150 }, + { date: '2024-04-02', desktop: 97, mobile: 180 }, + { date: '2024-04-03', desktop: 167, mobile: 120 }, + { date: '2024-04-04', desktop: 242, mobile: 260 }, + { date: '2024-04-05', desktop: 373, mobile: 290 }, + { date: '2024-04-06', desktop: 301, mobile: 340 }, + { date: '2024-04-07', desktop: 245, mobile: 180 }, + { date: '2024-04-08', desktop: 409, mobile: 320 }, + { date: '2024-04-09', desktop: 59, mobile: 110 }, + { date: '2024-04-10', desktop: 261, mobile: 190 }, + { date: '2024-04-11', desktop: 327, mobile: 350 }, + { date: '2024-04-12', desktop: 292, mobile: 210 }, + { date: '2024-04-13', desktop: 342, mobile: 380 }, + { date: '2024-04-14', desktop: 137, mobile: 220 }, + { date: '2024-04-15', desktop: 120, mobile: 170 }, + { date: '2024-04-16', desktop: 138, mobile: 190 }, + { date: '2024-04-17', desktop: 446, mobile: 360 }, + { date: '2024-04-18', desktop: 364, mobile: 410 }, + { date: '2024-04-19', desktop: 243, mobile: 180 }, + { date: '2024-04-20', desktop: 89, mobile: 150 }, + { date: '2024-04-21', desktop: 137, mobile: 200 }, + { date: '2024-04-22', desktop: 224, mobile: 170 }, + { date: '2024-04-23', desktop: 138, mobile: 230 }, + { date: '2024-04-24', desktop: 387, mobile: 290 }, + { date: '2024-04-25', desktop: 215, mobile: 250 }, + { date: '2024-04-26', desktop: 75, mobile: 130 }, + { date: '2024-04-27', desktop: 383, mobile: 420 }, + { date: '2024-04-28', desktop: 122, mobile: 180 }, + { date: '2024-04-29', desktop: 315, mobile: 240 }, + { date: '2024-04-30', desktop: 454, mobile: 380 }, + { date: '2024-05-01', desktop: 165, mobile: 220 }, + { date: '2024-05-02', desktop: 293, mobile: 310 }, + { date: '2024-05-03', desktop: 247, mobile: 190 }, + { date: '2024-05-04', desktop: 385, mobile: 420 }, + { date: '2024-05-05', desktop: 481, mobile: 390 }, + { date: '2024-05-06', desktop: 498, mobile: 520 }, + { date: '2024-05-07', desktop: 388, mobile: 300 }, + { date: '2024-05-08', desktop: 149, mobile: 210 }, + { date: '2024-05-09', desktop: 227, mobile: 180 }, + { date: '2024-05-10', desktop: 293, mobile: 330 }, + { date: '2024-05-11', desktop: 335, mobile: 270 }, + { date: '2024-05-12', desktop: 197, mobile: 240 }, + { date: '2024-05-13', desktop: 197, mobile: 160 }, + { date: '2024-05-14', desktop: 448, mobile: 490 }, + { date: '2024-05-15', desktop: 473, mobile: 380 }, + { date: '2024-05-16', desktop: 338, mobile: 400 }, + { date: '2024-05-17', desktop: 499, mobile: 420 }, + { date: '2024-05-18', desktop: 315, mobile: 350 }, + { date: '2024-05-19', desktop: 235, mobile: 180 }, + { date: '2024-05-20', desktop: 177, mobile: 230 }, + { date: '2024-05-21', desktop: 82, mobile: 140 }, + { date: '2024-05-22', desktop: 81, mobile: 120 }, + { date: '2024-05-23', desktop: 252, mobile: 290 }, + { date: '2024-05-24', desktop: 294, mobile: 220 }, + { date: '2024-05-25', desktop: 201, mobile: 250 }, + { date: '2024-05-26', desktop: 213, mobile: 170 }, + { date: '2024-05-27', desktop: 420, mobile: 460 }, + { date: '2024-05-28', desktop: 233, mobile: 190 }, + { date: '2024-05-29', desktop: 78, mobile: 130 }, + { date: '2024-05-30', desktop: 340, mobile: 280 }, + { date: '2024-05-31', desktop: 178, mobile: 230 }, + { date: '2024-06-01', desktop: 178, mobile: 200 }, + { date: '2024-06-02', desktop: 470, mobile: 410 }, + { date: '2024-06-03', desktop: 103, mobile: 160 }, + { date: '2024-06-04', desktop: 439, mobile: 380 }, + { date: '2024-06-05', desktop: 88, mobile: 140 }, + { date: '2024-06-06', desktop: 294, mobile: 250 }, + { date: '2024-06-07', desktop: 323, mobile: 370 }, + { date: '2024-06-08', desktop: 385, mobile: 320 }, + { date: '2024-06-09', desktop: 438, mobile: 480 }, + { date: '2024-06-10', desktop: 155, mobile: 200 }, + { date: '2024-06-11', desktop: 92, mobile: 150 }, + { date: '2024-06-12', desktop: 492, mobile: 420 }, + { date: '2024-06-13', desktop: 81, mobile: 130 }, + { date: '2024-06-14', desktop: 426, mobile: 380 }, + { date: '2024-06-15', desktop: 307, mobile: 350 }, + { date: '2024-06-16', desktop: 371, mobile: 310 }, + { date: '2024-06-17', desktop: 475, mobile: 520 }, + { date: '2024-06-18', desktop: 107, mobile: 170 }, + { date: '2024-06-19', desktop: 341, mobile: 290 }, + { date: '2024-06-20', desktop: 408, mobile: 450 }, + { date: '2024-06-21', desktop: 169, mobile: 210 }, + { date: '2024-06-22', desktop: 317, mobile: 270 }, + { date: '2024-06-23', desktop: 480, mobile: 530 }, + { date: '2024-06-24', desktop: 132, mobile: 180 }, + { date: '2024-06-25', desktop: 141, mobile: 190 }, + { date: '2024-06-26', desktop: 434, mobile: 380 }, + { date: '2024-06-27', desktop: 448, mobile: 490 }, + { date: '2024-06-28', desktop: 149, mobile: 200 }, + { date: '2024-06-29', desktop: 103, mobile: 160 }, + { date: '2024-06-30', desktop: 446, mobile: 400 }, + ]; + + const chartConfig = { + views: { + label: 'Page Views', + }, + desktop: { + label: 'Desktop', + color: 'hsl(var(--chart-1))', + }, + mobile: { + label: 'Mobile', + color: 'hsl(var(--chart-2))', + }, + } satisfies ChartConfig; + + const total = useMemo( + () => ({ + desktop: chartData.reduce((acc, curr) => acc + curr.desktop, 0), + mobile: chartData.reduce((acc, curr) => acc + curr.mobile, 0), + }), + [], + ); + + return ( + + +
+ Page Views + + + Showing total visitors for the last 3 months + +
+ +
+ {['desktop', 'mobile'].map((key) => { + const chart = key as keyof typeof chartConfig; + return ( + + ); + })} +
+
+ + + + + + { + const date = new Date(value); + return date.toLocaleDateString('en-US', { + month: 'short', + day: 'numeric', + }); + }} + /> + { + return new Date(value).toLocaleDateString('en-US', { + month: 'short', + day: 'numeric', + year: 'numeric', + }); + }} + /> + } + /> + + + + +
+ ); +} diff --git a/apps/web/app/home/[account]/billing/error.tsx b/apps/web/app/home/[account]/billing/error.tsx index 5eb160524..ef97b93c9 100644 --- a/apps/web/app/home/[account]/billing/error.tsx +++ b/apps/web/app/home/[account]/billing/error.tsx @@ -4,6 +4,7 @@ import { ExclamationTriangleIcon } from '@radix-ui/react-icons'; import { useCaptureException } from '@kit/monitoring/hooks'; import { Alert, AlertDescription, AlertTitle } from '@kit/ui/alert'; +import { AppBreadcrumbs } from '@kit/ui/app-breadcrumbs'; import { Button } from '@kit/ui/button'; import { PageBody, PageHeader } from '@kit/ui/page'; import { Trans } from '@kit/ui/trans'; @@ -20,8 +21,8 @@ export default function BillingErrorPage({ return ( <> } - description={} + title={} + description={} /> diff --git a/apps/web/app/home/[account]/billing/page.tsx b/apps/web/app/home/[account]/billing/page.tsx index 0351b8d15..d727f276d 100644 --- a/apps/web/app/home/[account]/billing/page.tsx +++ b/apps/web/app/home/[account]/billing/page.tsx @@ -6,11 +6,13 @@ import { CurrentSubscriptionCard, } from '@kit/billing-gateway/components'; import { Alert, AlertDescription, AlertTitle } from '@kit/ui/alert'; + import { If } from '@kit/ui/if'; import { PageBody } from '@kit/ui/page'; import { Trans } from '@kit/ui/trans'; import { cn } from '@kit/ui/utils'; +import { AppBreadcrumbs } from '@kit/ui/app-breadcrumbs'; import billingConfig from '~/config/billing.config'; import { createI18nServerInstance } from '~/lib/i18n/i18n.server'; import { withI18n } from '~/lib/i18n/with-i18n'; @@ -75,8 +77,8 @@ async function TeamAccountBillingPage({ params }: Params) { <> } - description={} + title={} + description={} /> diff --git a/apps/web/app/home/[account]/members/page.tsx b/apps/web/app/home/[account]/members/page.tsx index ef4391e10..03c68bece 100644 --- a/apps/web/app/home/[account]/members/page.tsx +++ b/apps/web/app/home/[account]/members/page.tsx @@ -18,6 +18,7 @@ import { If } from '@kit/ui/if'; import { PageBody } from '@kit/ui/page'; import { Trans } from '@kit/ui/trans'; +import { AppBreadcrumbs } from '@kit/ui/app-breadcrumbs'; import { createI18nServerInstance } from '~/lib/i18n/i18n.server'; import { withI18n } from '~/lib/i18n/with-i18n'; @@ -55,18 +56,18 @@ async function TeamAccountMembersPage({ params }: Params) { return ( <> } - description={} + title={} + description={} account={account.slug} /> -
+
- + diff --git a/apps/web/app/home/[account]/page.tsx b/apps/web/app/home/[account]/page.tsx index 943efb768..6329981a1 100644 --- a/apps/web/app/home/[account]/page.tsx +++ b/apps/web/app/home/[account]/page.tsx @@ -1,10 +1,8 @@ import loadDynamic from 'next/dynamic'; -import { PlusCircle } from 'lucide-react'; - -import { Button } from '@kit/ui/button'; +import { AppBreadcrumbs } from '@kit/ui/app-breadcrumbs'; +import { LoadingOverlay } from '@kit/ui/loading-overlay'; import { PageBody } from '@kit/ui/page'; -import { Spinner } from '@kit/ui/spinner'; import { Trans } from '@kit/ui/trans'; import { createI18nServerInstance } from '~/lib/i18n/i18n.server'; @@ -21,18 +19,11 @@ const DashboardDemo = loadDynamic( { ssr: false, loading: () => ( -
- - -
+ + -
-
+ + ), }, ); @@ -51,14 +42,9 @@ function TeamAccountHomePage({ params }: { params: Params }) { <> } - description={} - > - - + title={} + description={} + /> diff --git a/apps/web/app/home/[account]/settings/page.tsx b/apps/web/app/home/[account]/settings/page.tsx index f81a7de7d..321609d76 100644 --- a/apps/web/app/home/[account]/settings/page.tsx +++ b/apps/web/app/home/[account]/settings/page.tsx @@ -4,6 +4,7 @@ import { TeamAccountSettingsContainer } from '@kit/team-accounts/components'; import { PageBody } from '@kit/ui/page'; import { Trans } from '@kit/ui/trans'; +import { AppBreadcrumbs } from '@kit/ui/app-breadcrumbs'; import featuresFlagConfig from '~/config/feature-flags.config'; import pathsConfig from '~/config/paths.config'; import { createI18nServerInstance } from '~/lib/i18n/i18n.server'; @@ -51,7 +52,7 @@ async function TeamAccountSettingsPage(props: Props) { } - description={} + description={} /> diff --git a/apps/web/config/billing.sample.config.ts b/apps/web/config/billing.sample.config.ts index 23bac72b0..c97d705dd 100644 --- a/apps/web/config/billing.sample.config.ts +++ b/apps/web/config/billing.sample.config.ts @@ -128,7 +128,7 @@ export default createBillingSchema({ { id: 'price_enterprise_yearly', name: 'Base', - cost: 299.99, + cost: 299.90, type: 'flat', }, ], diff --git a/apps/web/config/personal-account-navigation.config.tsx b/apps/web/config/personal-account-navigation.config.tsx index f9a253429..05da6f358 100644 --- a/apps/web/config/personal-account-navigation.config.tsx +++ b/apps/web/config/personal-account-navigation.config.tsx @@ -9,13 +9,13 @@ const iconClasses = 'w-4'; const routes = [ { - label: 'common:homeTabLabel', + label: 'common:routes.home', path: pathsConfig.app.home, Icon: , end: true, }, { - label: 'account:accountTabLabel', + label: 'common:routes.account', path: pathsConfig.app.personalAccountSettings, Icon: , }, @@ -23,7 +23,7 @@ const routes = [ if (featureFlagsConfig.enablePersonalAccountBilling) { routes.push({ - label: 'common:billingTabLabel', + label: 'common:routes.billing', path: pathsConfig.app.personalAccountBilling, Icon: , }); diff --git a/apps/web/config/team-account-navigation.config.tsx b/apps/web/config/team-account-navigation.config.tsx index 139cd08f2..214b0fff4 100644 --- a/apps/web/config/team-account-navigation.config.tsx +++ b/apps/web/config/team-account-navigation.config.tsx @@ -9,28 +9,28 @@ const iconClasses = 'w-4'; const getRoutes = (account: string) => [ { - label: 'common:dashboardTabLabel', + label: 'common:routes.dashboard', path: pathsConfig.app.accountHome.replace('[account]', account), Icon: , end: true, }, { - label: 'common:settingsTabLabel', + label: 'common:routes.settings', collapsible: false, children: [ { - label: 'common:settingsTabLabel', + label: 'common:routes.settings', path: createPath(pathsConfig.app.accountSettings, account), Icon: , }, { - label: 'common:accountMembers', + label: 'common:routes.members', path: createPath(pathsConfig.app.accountMembers, account), Icon: , }, featureFlagsConfig.enableTeamAccountBilling ? { - label: 'common:billingTabLabel', + label: 'common:routes.billing', path: createPath(pathsConfig.app.accountBilling, account), Icon: , } diff --git a/apps/web/content/posts/must-have-features.mdoc b/apps/web/content/posts/must-have-features.mdoc index 0636c05fd..eb66b3983 100644 --- a/apps/web/content/posts/must-have-features.mdoc +++ b/apps/web/content/posts/must-have-features.mdoc @@ -8,8 +8,6 @@ publishedAt: 2024-04-12 status: "published" --- -# Brainstorming Ideas for Your Next Micro SaaS - ## Niche Service Solutions Consider identifying a specific niche or industry with underserved needs. Whether it's project management tools tailored for creative freelancers or appointment scheduling software for niche healthcare practitioners, targeting a specific market can lead to a loyal customer base hungry for tailored solutions. diff --git a/apps/web/public/locales/en/billing.json b/apps/web/public/locales/en/billing.json index 6cba529dd..bd10fb6b9 100644 --- a/apps/web/public/locales/en/billing.json +++ b/apps/web/public/locales/en/billing.json @@ -26,6 +26,7 @@ "month": "Billed monthly", "year": "Billed yearly" }, + "perMonth": "per month", "custom": "Custom Plan", "lifetime": "Lifetime", "trialPeriod": "{{period}} day trial", diff --git a/apps/web/public/locales/en/common.json b/apps/web/public/locales/en/common.json index 16632219e..b06cb9073 100644 --- a/apps/web/public/locales/en/common.json +++ b/apps/web/public/locales/en/common.json @@ -1,7 +1,7 @@ { "homeTabLabel": "Home", "homeTabDescription": "Welcome to your home page", - "accountMembers": "Members", + "accountMembers": "Team Members", "membersTabDescription": "Here you can manage the members of your team.", "billingTabLabel": "Billing", "billingTabDescription": "Manage your billing and subscription", @@ -55,6 +55,15 @@ "newVersionAvailableDescription": "A new version of the app is available. It is recommended to refresh the page to get the latest updates and avoid any issues.", "newVersionSubmitButton": "Reload and Update", "back": "Back", + "routes": { + "home": "Home", + "account": "Account", + "members": "Members", + "billing": "Billing", + "dashboard": "Dashboard", + "settings": "Settings", + "profile": "Profile" + }, "roles": { "owner": { "label": "Owner" diff --git a/apps/web/styles/globals.css b/apps/web/styles/globals.css index b60f344de..4d1a906c9 100644 --- a/apps/web/styles/globals.css +++ b/apps/web/styles/globals.css @@ -4,69 +4,58 @@ @layer base { :root { - --background: 0deg 0% 100%; - --foreground: 222.2deg 47.4% 11.2%; - - --muted: 210deg 40% 96.1%; - --muted-foreground: 215.4deg 16.3% 46.9%; - - --popover: 0deg 0% 100%; - --popover-foreground: 222.2deg 47.4% 11.2%; - - --border: 214.3deg 31.8% 94.4%; - --input: 214.3deg 31.8% 91.4%; - - --card: 0deg 0% 100%; - --card-foreground: 222.2deg 47.4% 11.2%; - - --primary: 222.2deg 47.4% 11.2%; - --primary-foreground: 210deg 40% 98%; - - --secondary: 210deg 40% 96.1%; - --secondary-foreground: 222.2deg 47.4% 11.2%; - - --accent: 210deg 40% 96.1%; - --accent-foreground: 222.2deg 47.4% 11.2%; - - --destructive: 0deg 100% 50%; - --destructive-foreground: 210deg 40% 98%; - - --ring: 215deg 20.2% 65.1%; - + --background: 0 0% 100%; + --foreground: 224 71.4% 4.1%; + --card: 0 0% 100%; + --card-foreground: 224 71.4% 4.1%; + --popover: 0 0% 100%; + --popover-foreground: 224 71.4% 4.1%; + --primary: 220.9 39.3% 11%; + --primary-foreground: 210 20% 98%; + --secondary: 220 14.3% 95.9%; + --secondary-foreground: 220.9 39.3% 11%; + --muted: 220 14.3% 95.9%; + --muted-foreground: 220 8.9% 46.1%; + --accent: 220 14.3% 95.9%; + --accent-foreground: 220.9 39.3% 11%; + --destructive: 0 84.2% 60.2%; + --destructive-foreground: 210 20% 98%; + --border: 214.3 31.8% 94.4%; + --input: 214.3 31.8% 91.4%; + --ring: 224 71.4% 4.1%; --radius: 0.5rem; + --chart-1: 12 76% 61%; + --chart-2: 173 58% 39%; + --chart-3: 197 37% 24%; + --chart-4: 43 74% 66%; + --chart-5: 27 87% 67%; } .dark { - --background: 224 71% 4%; - --foreground: 213 31% 91%; - - --muted: 223 47% 11%; - --muted-foreground: 215.4 16.3% 56.9%; - - --accent: 216 34% 10%; - --accent-foreground: 210 40% 98%; - - --popover: 224 71% 4%; - --popover-foreground: 215 20.2% 65.1%; - - --border: 216 34% 17%; - --input: 216 34% 17%; - - --card: 224 71% 4%; - --card-foreground: 213 31% 91%; - - --primary: 210 40% 98%; - --primary-foreground: 222.2 47.4% 1.2%; - - --secondary: 216 34% 10%; - --secondary-foreground: 210 40% 98%; - - --destructive: 0 63% 31%; - --destructive-foreground: 210 40% 98%; - - --ring: 216 34% 17%; - - --radius: 0.5rem; + --background: 224 71.4% 4.1%; + --foreground: 210 20% 98%; + --card: 224 71.4% 4.1%; + --card-foreground: 210 20% 98%; + --popover: 224 71.4% 4.1%; + --popover-foreground: 210 20% 98%; + --primary: 210 20% 98%; + --primary-foreground: 220.9 39.3% 11%; + --secondary: 215 27.9% 13%; + --secondary-foreground: 210 20% 98%; + --muted: 215 27.9% 13%; + --muted-foreground: 217.9 10.6% 64.9%; + --accent: 215 27.9% 13%; + --accent-foreground: 210 20% 98%; + --destructive: 0 62.8% 30.6%; + --destructive-foreground: 210 20% 98%; + --border: 215 27.9% 13%; + --input: 215 27.9% 13%; + --ring: 216 12.2% 83.9%; + --chart-1: 220 70% 50%; + --chart-2: 160 60% 45%; + --chart-3: 30 80% 55%; + --chart-4: 280 65% 60%; + --chart-5: 340 75% 55%; } } diff --git a/apps/web/supabase/config.toml b/apps/web/supabase/config.toml index 4c75726ea..594d5a496 100644 --- a/apps/web/supabase/config.toml +++ b/apps/web/supabase/config.toml @@ -89,4 +89,7 @@ content_path = "./supabase/templates/change-email-address.html" [auth.email.template.magic_link] subject = "Sign in to Makerkit" -content_path = "./supabase/templates/magic-link.html" \ No newline at end of file +content_path = "./supabase/templates/magic-link.html" + +[analytics] +enabled = false \ No newline at end of file diff --git a/packages/billing/gateway/src/components/line-item-details.tsx b/packages/billing/gateway/src/components/line-item-details.tsx index 9337958b3..7a26745e3 100644 --- a/packages/billing/gateway/src/components/line-item-details.tsx +++ b/packages/billing/gateway/src/components/line-item-details.tsx @@ -1,4 +1,7 @@ +'use client'; + import { PlusSquare } from 'lucide-react'; +import { useTranslation } from 'react-i18next'; import { z } from 'zod'; import type { LineItemSchema } from '@kit/billing'; @@ -16,6 +19,9 @@ export function LineItemDetails( selectedInterval?: string | undefined; }>, ) { + const locale = useTranslation().i18n.language; + const currencyCode = props?.currency.toLowerCase(); + return (
{props.lineItems.map((item, index) => { @@ -48,10 +54,11 @@ export function LineItemDetails( @@ -89,7 +96,11 @@ export function LineItemDetails( - - {formatCurrency(props?.currency.toLowerCase(), item.cost)} + {formatCurrency({ + currencyCode, + value: item.cost, + locale, + })}
@@ -129,7 +140,11 @@ export function LineItemDetails( - {formatCurrency(props.currency.toLowerCase(), item.cost)} + {formatCurrency({ + currencyCode, + value: item.cost, + locale, + })}
@@ -165,7 +180,11 @@ export function LineItemDetails( {/* If there are no tiers, there is a flat cost for usage */} - {formatCurrency(props?.currency.toLowerCase(), item.cost)} + {formatCurrency({ + currencyCode, + value: item.cost, + locale, + })}
@@ -203,6 +222,7 @@ function Tiers({ item: z.infer; }) { const unit = item.unit; + const locale = useTranslation().i18n.language; const tiers = item.tiers?.map((tier, index) => { const tiersLength = item.tiers?.length ?? 0; @@ -228,7 +248,11 @@ function Tiers({ - {formatCurrency(currency.toLowerCase(), tier.cost)} + {formatCurrency({ + currencyCode: currency.toLowerCase(), + value: tier.cost, + locale, + })} 1}> @@ -264,7 +288,11 @@ function Tiers({ - {formatCurrency(currency.toLowerCase(), tier.cost)} + {formatCurrency({ + currencyCode: currency.toLowerCase(), + value: tier.cost, + locale, + })} diff --git a/packages/billing/gateway/src/components/plan-picker.tsx b/packages/billing/gateway/src/components/plan-picker.tsx index 8ecfc2cbe..d34bb47b9 100644 --- a/packages/billing/gateway/src/components/plan-picker.tsx +++ b/packages/billing/gateway/src/components/plan-picker.tsx @@ -26,7 +26,6 @@ import { FormLabel, FormMessage, } from '@kit/ui/form'; -import { Heading } from '@kit/ui/heading'; import { If } from '@kit/ui/if'; import { Label } from '@kit/ui/label'; import { @@ -106,6 +105,8 @@ export function PlanPicker( const isRecurringPlan = selectedPlan?.paymentType === 'recurring' || !selectedPlan; + const locale = useTranslation().i18n.language; + return (
- {formatCurrency( - product.currency.toLowerCase(), - primaryLineItem.cost, - )} + {formatCurrency({ + currencyCode: + product.currency.toLowerCase(), + value: primaryLineItem.cost, + locale, + })} @@ -424,7 +427,7 @@ function PlanDetails({ } >
- + / - +

- + {props.children} diff --git a/packages/billing/gateway/src/components/pricing-table.tsx b/packages/billing/gateway/src/components/pricing-table.tsx index 23c49ab80..fde476b2e 100644 --- a/packages/billing/gateway/src/components/pricing-table.tsx +++ b/packages/billing/gateway/src/components/pricing-table.tsx @@ -29,6 +29,8 @@ interface Paths { return: string; } +type Interval = 'month' | 'year'; + export function PricingTable({ config, paths, @@ -48,7 +50,7 @@ export function PricingTable({ highlighted?: boolean; }>; }) { - const intervals = getPlanIntervals(config).filter(Boolean) as string[]; + const intervals = getPlanIntervals(config).filter(Boolean) as Interval[]; const [interval, setInterval] = useState(intervals[0]!); return ( @@ -123,7 +125,7 @@ function PricingItem( plan: { id: string; lineItems: z.infer[]; - interval?: string; + interval?: Interval; name?: string; href?: string; label?: string; @@ -156,6 +158,8 @@ function PricingItem( return item.type !== 'flat'; }); + const interval = props.plan.interval as Interval; + return (

-
+
- {lineItem ? ( - formatCurrency(props.product.currency, lineItem.cost) - ) : props.plan.label ? ( - - ) : ( - - )} + @@ -337,15 +340,19 @@ function FeaturesList( function Price({ children }: React.PropsWithChildren) { return (
{children} + + + +
); } @@ -368,9 +375,9 @@ function ListItem({ children }: React.PropsWithChildren) { function PlanIntervalSwitcher( props: React.PropsWithChildren<{ - intervals: string[]; - interval: string; - setInterval: (interval: string) => void; + intervals: Interval[]; + interval: Interval; + setInterval: (interval: Interval) => void; }>, ) { return ( @@ -470,3 +477,40 @@ function DefaultCheckoutButton( ); } + +function LineItemPrice({ + lineItem, + plan, + interval, + product, +}: { + lineItem: z.infer | undefined; + plan: { + label?: string; + }; + interval: Interval | undefined; + product: { + currency: string; + }; +}) { + const { i18n } = useTranslation(); + const isYearlyPricing = interval === 'year'; + + const cost = lineItem + ? isYearlyPricing + ? Number(lineItem.cost / 12).toFixed(2) + : lineItem?.cost + : 0; + + const costString = lineItem && formatCurrency({ + currencyCode: product.currency, + locale: i18n.language, + value: cost, + }); + + const labelString = plan.label && ( + + ); + + return costString ?? labelString ?? ; +} \ No newline at end of file diff --git a/packages/billing/gateway/tsconfig.json b/packages/billing/gateway/tsconfig.json index c4697e934..a8cb9ab08 100644 --- a/packages/billing/gateway/tsconfig.json +++ b/packages/billing/gateway/tsconfig.json @@ -1,7 +1,8 @@ { "extends": "@kit/tsconfig/base.json", "compilerOptions": { - "tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json" + "tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json", + "types": ["react/experimental"] }, "include": ["*.ts", "src"], "exclude": ["node_modules"] diff --git a/packages/features/accounts/src/components/account-selector.tsx b/packages/features/accounts/src/components/account-selector.tsx index f19a3ef2e..dc6ad50fa 100644 --- a/packages/features/accounts/src/components/account-selector.tsx +++ b/packages/features/accounts/src/components/account-selector.tsx @@ -103,7 +103,7 @@ export function AccountSelector({ role="combobox" aria-expanded={open} className={cn( - 'dark:shadow-primary/10 group w-auto min-w-0 max-w-fit px-2', + 'dark:shadow-primary/10 group w-full lg:w-auto min-w-0 lg:max-w-fit px-2', { 'justify-start': !collapsed, 'justify-center': collapsed, diff --git a/packages/features/accounts/src/components/personal-account-dropdown.tsx b/packages/features/accounts/src/components/personal-account-dropdown.tsx index f52ffb9fe..135fe37bd 100644 --- a/packages/features/accounts/src/components/personal-account-dropdown.tsx +++ b/packages/features/accounts/src/components/personal-account-dropdown.tsx @@ -93,6 +93,7 @@ export function PersonalAccountDropdown({ )} > @@ -153,7 +154,7 @@ export function PersonalAccountDropdown({ - + diff --git a/packages/features/accounts/src/components/personal-account-settings/account-settings-container.tsx b/packages/features/accounts/src/components/personal-account-settings/account-settings-container.tsx index bc5611867..45f4e4662 100644 --- a/packages/features/accounts/src/components/personal-account-settings/account-settings-container.tsx +++ b/packages/features/accounts/src/components/personal-account-settings/account-settings-container.tsx @@ -43,7 +43,7 @@ export function PersonalAccountSettingsContainer( } return ( -
+
@@ -148,7 +148,7 @@ export function PersonalAccountSettingsContainer( - + diff --git a/packages/features/admin/src/components/admin-account-page.tsx b/packages/features/admin/src/components/admin-account-page.tsx index 602e16771..e1ffaf11d 100644 --- a/packages/features/admin/src/components/admin-account-page.tsx +++ b/packages/features/admin/src/components/admin-account-page.tsx @@ -1,8 +1,15 @@ -import { BadgeX, Ban, ShieldPlus, VenetianMask } from 'lucide-react'; +import { + BadgeX, + Ban, + CreditCardIcon, + ShieldPlus, + VenetianMask, +} from 'lucide-react'; import { Database } from '@kit/supabase/database'; import { getSupabaseServerComponentClient } from '@kit/supabase/server-component-client'; import { Alert, AlertDescription, AlertTitle } from '@kit/ui/alert'; +import { AppBreadcrumbs } from '@kit/ui/app-breadcrumbs'; import { Badge } from '@kit/ui/badge'; import { Button } from '@kit/ui/button'; import { Heading } from '@kit/ui/heading'; @@ -58,6 +65,13 @@ async function PersonalAccountPage(props: { account: Account }) { return (
+ +
@@ -66,7 +80,9 @@ async function PersonalAccountPage(props: { account: Account }) { displayName={props.account.name} /> - {props.account.name} + + {props.account.name} +
Personal Account @@ -115,9 +131,7 @@ async function PersonalAccountPage(props: { account: Account }) {
- - Teams - + Teams
@@ -135,6 +149,13 @@ async function TeamAccountPage(props: { return (
+ +
@@ -143,7 +164,9 @@ async function TeamAccountPage(props: { displayName={props.account.name} /> - {props.account.name} + + {props.account.name} +
Team Account @@ -162,9 +185,7 @@ async function TeamAccountPage(props: {
- - Team Members - + Team Members
@@ -199,14 +220,14 @@ async function SubscriptionsTable(props: { accountId: string }) { return (
- - Subscription - + Subscription + + + No subscription found for this account. diff --git a/packages/features/admin/src/components/admin-accounts-table.tsx b/packages/features/admin/src/components/admin-accounts-table.tsx index d0af34f84..b937f0949 100644 --- a/packages/features/admin/src/components/admin-accounts-table.tsx +++ b/packages/features/admin/src/components/admin-accounts-table.tsx @@ -58,7 +58,9 @@ export function AdminAccountsTable( ) { return (
- +
+ +
-
- - onSubmit(data))} - > - { + form.setValue( + 'type', + value as z.infer['type'], + { + shouldValidate: true, + shouldDirty: true, + shouldTouch: true, + }, + ); - return onSubmit(form.getValues()); - }} - > - - - + return onSubmit(form.getValues()); + }} + > + + + - - - Account Type + + + Account Type - All accounts - Team - Personal - - - + All accounts + Team + Personal + + + - ( - - - - - - )} - /> - - -
-
+ ( + + + + + + )} + /> + + ); } diff --git a/packages/features/auth/src/components/update-password-form.tsx b/packages/features/auth/src/components/update-password-form.tsx index 73f7843c6..de8355d1e 100644 --- a/packages/features/auth/src/components/update-password-form.tsx +++ b/packages/features/auth/src/components/update-password-form.tsx @@ -47,7 +47,7 @@ export function UpdatePasswordForm(params: { redirectTo: string }) { return (
- +
diff --git a/packages/features/team-accounts/src/components/settings/team-account-settings-container.tsx b/packages/features/team-accounts/src/components/settings/team-account-settings-container.tsx index 199da27c6..6597f0ffb 100644 --- a/packages/features/team-accounts/src/components/settings/team-account-settings-container.tsx +++ b/packages/features/team-accounts/src/components/settings/team-account-settings-container.tsx @@ -31,7 +31,7 @@ export function TeamAccountSettingsContainer(props: { } }) { return ( -
+
@@ -67,7 +67,7 @@ export function TeamAccountSettingsContainer(props: { - + diff --git a/packages/shared/src/utils.ts b/packages/shared/src/utils.ts index a17807e98..a9d6a26f2 100644 --- a/packages/shared/src/utils.ts +++ b/packages/shared/src/utils.ts @@ -6,12 +6,16 @@ export function isBrowser() { } /** - *@name formatCurrency + * @name formatCurrency * @description Format the currency based on the currency code */ -export function formatCurrency(currencyCode: string, value: string | number) { - return new Intl.NumberFormat('en-US', { +export function formatCurrency(params: { + currencyCode: string; + locale: string; + value: string | number; +}) { + return new Intl.NumberFormat(params.locale, { style: 'currency', - currency: currencyCode, - }).format(Number(value)); + currency: params.currencyCode, + }).format(Number(params.value)); } diff --git a/packages/supabase/src/check-requires-mfa.ts b/packages/supabase/src/check-requires-mfa.ts index ae8c8a0c7..8e3bc9dfc 100644 --- a/packages/supabase/src/check-requires-mfa.ts +++ b/packages/supabase/src/check-requires-mfa.ts @@ -11,8 +11,16 @@ const ASSURANCE_LEVEL_2 = 'aal2'; export async function checkRequiresMultiFactorAuthentication( client: SupabaseClient, ) { + // Suppress the getSession warning. Remove when the issue is fixed. + // https://github.com/supabase/auth-js/issues/873 + // @ts-expect-error: suppressGetSessionWarning is not part of the public API + client.auth.suppressGetSessionWarning = true; + const assuranceLevel = await client.auth.mfa.getAuthenticatorAssuranceLevel(); + // @ts-expect-error: suppressGetSessionWarning is not part of the public API + client.auth.suppressGetSessionWarning = false; + if (assuranceLevel.error) { throw new Error(assuranceLevel.error.message); } diff --git a/packages/ui/package.json b/packages/ui/package.json index 24de2f7a7..fea81588e 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -33,6 +33,7 @@ "input-otp": "1.2.4", "lucide-react": "^0.418.0", "react-top-loading-bar": "2.3.1", + "recharts": "^2.12.7", "tailwind-merge": "^2.4.0" }, "devDependencies": { @@ -100,6 +101,8 @@ "./input-otp": "./src/shadcn/input-otp.tsx", "./textarea": "./src/shadcn/textarea.tsx", "./switch": "./src/shadcn/switch.tsx", + "./breadcrumb": "./src/shadcn/breadcrumb.tsx", + "./chart": "./src/shadcn/chart.tsx", "./utils": "./src/utils/index.ts", "./if": "./src/makerkit/if.tsx", "./trans": "./src/makerkit/trans.tsx", @@ -122,6 +125,8 @@ "./card-button": "./src/makerkit/card-button.tsx", "./version-updater": "./src/makerkit/version-updater.tsx", "./multi-step-form": "./src/makerkit/multi-step-form.tsx", + "./app-breadcrumbs": "./src/makerkit/app-breadcrumbs.tsx", + "./empty-state": "./src/makerkit/empty-state.tsx", "./marketing": "./src/makerkit/marketing/index.tsx" }, "typesVersions": { diff --git a/packages/ui/src/makerkit/app-breadcrumbs.tsx b/packages/ui/src/makerkit/app-breadcrumbs.tsx new file mode 100644 index 000000000..4e120805d --- /dev/null +++ b/packages/ui/src/makerkit/app-breadcrumbs.tsx @@ -0,0 +1,88 @@ +'use client'; + +import { usePathname } from 'next/navigation'; + +import { + Breadcrumb, + BreadcrumbEllipsis, + BreadcrumbItem, + BreadcrumbLink, + BreadcrumbList, + BreadcrumbSeparator, +} from '../shadcn/breadcrumb'; +import { If } from './if'; +import { Trans } from './trans'; +import { Fragment } from 'react'; + +const unslugify = (slug: string) => slug.replace(/-/g, ' '); + +export function AppBreadcrumbs(props: { + values?: Record; + maxDepth?: number; +}) { + const pathName = usePathname(); + const splitPath = pathName.split('/').filter(Boolean); + const values = props.values ?? {}; + const maxDepth = props.maxDepth ?? 6; + + const Ellipsis = ( + + + + ); + + const showEllipsis = splitPath.length > maxDepth; + + const visiblePaths = showEllipsis + ? ([splitPath[0], ...splitPath.slice(-maxDepth + 1)] as string[]) + : splitPath; + + return ( + + + {visiblePaths.map((path, index) => { + const label = + path in values ? ( + values[path] + ) : ( + + ); + + return ( + + + + + {label} + + + + + {index === 0 && showEllipsis && ( + <> + + {Ellipsis} + + )} + + + + + + ); + })} + + + ); +} diff --git a/packages/ui/src/makerkit/empty-state.tsx b/packages/ui/src/makerkit/empty-state.tsx new file mode 100644 index 000000000..dba3bf134 --- /dev/null +++ b/packages/ui/src/makerkit/empty-state.tsx @@ -0,0 +1,84 @@ +import React from 'react'; + +import { Button } from '../shadcn/button'; +import { cn } from '../utils'; + +const EmptyStateHeading = React.forwardRef< + HTMLHeadingElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +

+)); +EmptyStateHeading.displayName = 'EmptyStateHeading'; + +const EmptyStateText = React.forwardRef< + HTMLParagraphElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +

+)); +EmptyStateText.displayName = 'EmptyStateText'; + +const EmptyStateButton = React.forwardRef< + HTMLButtonElement, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( +

diff --git a/packages/ui/src/makerkit/loading-overlay.tsx b/packages/ui/src/makerkit/loading-overlay.tsx index c990fc55d..4974f0a92 100644 --- a/packages/ui/src/makerkit/loading-overlay.tsx +++ b/packages/ui/src/makerkit/loading-overlay.tsx @@ -27,7 +27,7 @@ export function LoadingOverlay({ > -
{children}
+
{children}
); } diff --git a/packages/ui/src/makerkit/page.tsx b/packages/ui/src/makerkit/page.tsx index 52db7850e..351812f49 100644 --- a/packages/ui/src/makerkit/page.tsx +++ b/packages/ui/src/makerkit/page.tsx @@ -13,9 +13,6 @@ type PageProps = React.PropsWithChildren<{ export function Page(props: PageProps) { switch (props.style) { - case 'sidebar': - return ; - case 'header': return ; @@ -31,7 +28,9 @@ function PageWithSidebar(props: PageProps) { const { Navigation, Children, MobileNavigation } = getSlotsFromPage(props); return ( -
+
{Navigation}
{MobileNavigation} -
{Children}
+
+ {Children} +
); @@ -54,7 +59,7 @@ export function PageMobileNavigation( }>, ) { return ( -
+
{props.children}
); @@ -72,7 +77,7 @@ function PageWithHeader(props: PageProps) { >
-
- {Children} -
+
{Children}
); @@ -115,13 +114,11 @@ export function PageNavigation(props: React.PropsWithChildren) { export function PageDescription(props: React.PropsWithChildren) { return ( -

- +
+
{props.children} - -

+
+
); } @@ -129,7 +126,7 @@ export function PageTitle(props: React.PropsWithChildren) { return (

{props.children} @@ -137,6 +134,10 @@ export function PageTitle(props: React.PropsWithChildren) { ); } +export function PageHeaderActions(props: React.PropsWithChildren) { + return
{props.children}
; +} + export function PageHeader({ children, title, @@ -150,17 +151,14 @@ export function PageHeader({ return (
- {title ? ( -
- {title} - - {description} -
- ) : null} +
+ {description} + {title} +
{children}
diff --git a/packages/ui/src/makerkit/profile-avatar.tsx b/packages/ui/src/makerkit/profile-avatar.tsx index 0ccc6671f..d969e65a8 100644 --- a/packages/ui/src/makerkit/profile-avatar.tsx +++ b/packages/ui/src/makerkit/profile-avatar.tsx @@ -1,4 +1,5 @@ import { Avatar, AvatarFallback, AvatarImage } from '../shadcn/avatar'; +import {cn} from "../utils"; type SessionProps = { displayName: string | null; @@ -9,10 +10,12 @@ type TextProps = { text: string; }; -type ProfileAvatarProps = SessionProps | TextProps; +type ProfileAvatarProps = (SessionProps | TextProps) & { + className?: string; +}; export function ProfileAvatar(props: ProfileAvatarProps) { - const avatarClassName = 'mx-auto w-9 h-9 group-focus:ring-2'; + const avatarClassName = cn(props.className, 'mx-auto w-9 h-9 group-focus:ring-2'); if ('text' in props) { return ( diff --git a/packages/ui/src/makerkit/sidebar.tsx b/packages/ui/src/makerkit/sidebar.tsx index b0eadc175..449d0d86d 100644 --- a/packages/ui/src/makerkit/sidebar.tsx +++ b/packages/ui/src/makerkit/sidebar.tsx @@ -60,7 +60,7 @@ export function SidebarContent({ className?: string; }>) { return ( -
+
{children}
); @@ -167,8 +167,9 @@ export function SidebarItem({ return (
); -} +} \ No newline at end of file diff --git a/packages/ui/src/shadcn/breadcrumb.tsx b/packages/ui/src/shadcn/breadcrumb.tsx new file mode 100644 index 000000000..8c232a89f --- /dev/null +++ b/packages/ui/src/shadcn/breadcrumb.tsx @@ -0,0 +1,116 @@ +import * as React from 'react'; + +import { ChevronRightIcon, DotsHorizontalIcon } from '@radix-ui/react-icons'; +import { Slot } from '@radix-ui/react-slot'; + +import { cn } from '../utils'; + +const Breadcrumb = React.forwardRef< + HTMLElement, + React.ComponentPropsWithoutRef<'nav'> & { + separator?: React.ReactNode; + } +>(({ ...props }, ref) =>