From e696f1aed0340c59702878cb7f538b808fe139d3 Mon Sep 17 00:00:00 2001 From: Giancarlo Buomprisco Date: Sun, 4 Aug 2024 23:25:28 +0800 Subject: [PATCH] Design Updates: Breadcrumbs, Empty State, new Charts and new colors Design Updates: Breadcrumbs, Empty State, new Charts and new colors * Add Breadcrumb component to UI package * Add AppBreadcrumbs for improved navigation: Replaced static text descriptions with the new AppBreadcrumbs component across multiple pages to enhance navigation. Addressed an issue with Supabase client warnings by temporarily suppressing getSession warnings. Also made minor UI adjustments, including adjustments to heading styles and layout features. * Enhance UI styling and configuration settings: Updated various UI components and global styles to improve styling consistency and responsiveness. * Update global styles and adjust padding: Updated several CSS variables for improved color accuracy and appearance. Added padding to admin account page body for better layout consistency. * Refactor UI components and adjust styling: Replaced Heading tags in Plan Picker with span for consistency. Added active and hover states to buttons in the sidebar. Refined background, layout styling, and color schemes across various components. Removed sidebar case in Page component switch statement. * Add Chart Components and Integrate into Dashboard: Introduced `recharts` library and created `Chart` components. Updated dashboard to use the new components and enhanced UI/UX with descriptions and restructured cards. * Enhance dashboard demo UI layout: Refactor the layout by adjusting flex properties and spacing classes to improve component alignment. Update dummy data generation and Figure font size for better visual consistency. * Update localization keys for navigation labels: Changed localization keys for tab labels to use 'routes' prefix for consistency. Adjusted corresponding component references and added missing keys for routes. This ensures better organization and uniformity in the code. * Add EmptyState component and enhance account handling: Introduced a new EmptyState component for UI consistency and updated JSON locales with 'account' route. Modified HomeAddAccountButton to accept className prop and refactored HomeAccountsListEmptyState to use the new EmptyState component. Updated navigation config to align labels in locales. * Add locale support and enhance currency formatting: This commit introduces locale-based currency formatting across billing components by utilizing the `useTranslation` hook to fetch the current language. It also refactors the `formatCurrency` function to accept an object parameter for better readability and reusability. * Fix typo in devDependencies section of template generator: Corrected a syntax error in `package.json.hbs` template affecting the `@kit/tsconfig` entry. The change ensures that the dependency is properly defined and prevents potential issues during package management. * Update heading levels and add tracking-tight class in auth shell: Changed Heading components from level 4 to level 5 and added the 'tracking-tight' class in multiple auth-related pages. This improves visual consistency and better aligns the typography across the application. --- .../_components/site-page-header.tsx | 18 +- .../blog/_components/html-renderer.module.css | 4 +- .../app/(marketing)/docs/[...slug]/page.tsx | 9 +- apps/web/app/(marketing)/docs/page.tsx | 22 +- apps/web/app/auth/password-reset/page.tsx | 2 +- apps/web/app/auth/sign-in/page.tsx | 2 +- apps/web/app/auth/sign-up/page.tsx | 2 +- .../(user)/_components/home-accounts-list.tsx | 30 +- .../_components/home-add-account-button.tsx | 7 +- apps/web/app/home/(user)/billing/page.tsx | 5 +- apps/web/app/home/(user)/page.tsx | 2 +- apps/web/app/home/(user)/settings/layout.tsx | 5 +- .../[account]/_components/dashboard-demo.tsx | 912 ++++++++++++++---- apps/web/app/home/[account]/billing/error.tsx | 5 +- apps/web/app/home/[account]/billing/page.tsx | 6 +- apps/web/app/home/[account]/members/page.tsx | 9 +- apps/web/app/home/[account]/page.tsx | 32 +- apps/web/app/home/[account]/settings/page.tsx | 3 +- apps/web/config/billing.sample.config.ts | 2 +- .../personal-account-navigation.config.tsx | 6 +- .../config/team-account-navigation.config.tsx | 10 +- .../web/content/posts/must-have-features.mdoc | 2 - apps/web/public/locales/en/billing.json | 1 + apps/web/public/locales/en/common.json | 11 +- apps/web/styles/globals.css | 107 +- apps/web/supabase/config.toml | 5 +- .../src/components/line-item-details.tsx | 46 +- .../gateway/src/components/plan-picker.tsx | 21 +- .../gateway/src/components/pricing-table.tsx | 74 +- packages/billing/gateway/tsconfig.json | 3 +- .../src/components/account-selector.tsx | 2 +- .../components/personal-account-dropdown.tsx | 3 +- .../account-settings-container.tsx | 4 +- .../src/components/admin-account-page.tsx | 47 +- .../src/components/admin-accounts-table.tsx | 104 +- .../src/components/update-password-form.tsx | 2 +- .../team-account-settings-container.tsx | 4 +- packages/shared/src/utils.ts | 14 +- packages/supabase/src/check-requires-mfa.ts | 8 + packages/ui/package.json | 5 + packages/ui/src/makerkit/app-breadcrumbs.tsx | 88 ++ packages/ui/src/makerkit/empty-state.tsx | 84 ++ packages/ui/src/makerkit/global-loader.tsx | 3 + packages/ui/src/makerkit/loading-overlay.tsx | 2 +- packages/ui/src/makerkit/page.tsx | 56 +- packages/ui/src/makerkit/profile-avatar.tsx | 7 +- packages/ui/src/makerkit/sidebar.tsx | 7 +- packages/ui/src/makerkit/spinner.tsx | 4 +- packages/ui/src/shadcn/breadcrumb.tsx | 116 +++ packages/ui/src/shadcn/card.tsx | 2 +- packages/ui/src/shadcn/chart.tsx | 366 +++++++ pnpm-lock.yaml | 15 +- .../templates/package/package.json.hbs | 4 +- 53 files changed, 1795 insertions(+), 515 deletions(-) create mode 100644 packages/ui/src/makerkit/app-breadcrumbs.tsx create mode 100644 packages/ui/src/makerkit/empty-state.tsx create mode 100644 packages/ui/src/shadcn/breadcrumb.tsx create mode 100644 packages/ui/src/shadcn/chart.tsx 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) =>