diff --git a/apps/web/app/(marketing)/docs/_components/docs-navigation.tsx b/apps/web/app/(marketing)/docs/_components/docs-navigation.tsx
index 589396bcf..4c779eccf 100644
--- a/apps/web/app/(marketing)/docs/_components/docs-navigation.tsx
+++ b/apps/web/app/(marketing)/docs/_components/docs-navigation.tsx
@@ -24,7 +24,7 @@ function DocsNavLink({
level: number;
activePath: string;
}) {
- const isCurrent = isRouteActive(url, activePath, 0);
+ const isCurrent = isRouteActive(url, activePath, true);
const isFirstLevel = level === 0;
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
new file mode 100644
index 000000000..f634e931e
--- /dev/null
+++ b/apps/web/app/home/(user)/_components/home-accounts-list.tsx
@@ -0,0 +1,50 @@
+import { use } from 'react';
+
+import Link from 'next/link';
+
+import { CardButton, CardButtonHeader } from '@kit/ui/card-button';
+import { Heading } from '@kit/ui/heading';
+
+import { loadUserWorkspace } from '../_lib/server/load-user-workspace';
+import { HomeAddAccountButton } from './home-add-account-button';
+
+export function HomeAccountsList() {
+ const { accounts } = use(loadUserWorkspace());
+
+ if (!accounts.length) {
+ return ;
+ }
+
+ return (
+
+
+ {accounts.map((account) => (
+
+
+ {account.label}
+
+
+ ))}
+
+
+ );
+}
+
+function HomeAccountsListEmptyState() {
+ return (
+
+
+ You don't have any teams yet.
+
+
+ Create a team to get started.
+
+
+
+
+
+ );
+}
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
new file mode 100644
index 000000000..401c2dc1c
--- /dev/null
+++ b/apps/web/app/home/(user)/_components/home-add-account-button.tsx
@@ -0,0 +1,23 @@
+'use client';
+
+import { useState } from 'react';
+
+import { CreateTeamAccountDialog } from '@kit/team-accounts/components';
+import { Button } from '@kit/ui/button';
+
+export function HomeAddAccountButton() {
+ const [isAddingAccount, setIsAddingAccount] = useState(false);
+
+ return (
+ <>
+
+
+
+ >
+ );
+}
diff --git a/apps/web/app/home/(user)/_components/home-menu-navigation.tsx b/apps/web/app/home/(user)/_components/home-menu-navigation.tsx
index 9f93c7e45..8d91657c4 100644
--- a/apps/web/app/home/(user)/_components/home-menu-navigation.tsx
+++ b/apps/web/app/home/(user)/_components/home-menu-navigation.tsx
@@ -22,7 +22,7 @@ export function HomeMenuNavigation(props: { workspace: UserWorkspace }) {
path: string;
label: string;
Icon?: React.ReactNode;
- end?: boolean | undefined;
+ end?: boolean | ((path: string) => boolean);
}>
>((acc, item) => {
if ('children' in item) {
diff --git a/apps/web/app/home/[account]/_components/team-account-navigation-menu.tsx b/apps/web/app/home/[account]/_components/team-account-navigation-menu.tsx
index 199b164ee..a1926296e 100644
--- a/apps/web/app/home/[account]/_components/team-account-navigation-menu.tsx
+++ b/apps/web/app/home/[account]/_components/team-account-navigation-menu.tsx
@@ -22,7 +22,7 @@ export function TeamAccountNavigationMenu(props: {
path: string;
label: string;
Icon?: React.ReactNode;
- end?: boolean | undefined;
+ end?: boolean | ((path: string) => boolean);
}>
>((acc, item) => {
if ('children' in item) {
diff --git a/packages/billing/core/src/create-billing-schema.ts b/packages/billing/core/src/create-billing-schema.ts
index 3fe45709c..5c58d3817 100644
--- a/packages/billing/core/src/create-billing-schema.ts
+++ b/packages/billing/core/src/create-billing-schema.ts
@@ -89,6 +89,7 @@ export const PlanSchema = z
interval: BillingIntervalSchema.optional(),
custom: z.boolean().default(false).optional(),
label: z.string().min(1).optional(),
+ buttonLabel: z.string().min(1).optional(),
href: z.string().min(1).optional(),
lineItems: z.array(LineItemSchema).refine(
(schema) => {
diff --git a/packages/billing/gateway/src/components/pricing-table.tsx b/packages/billing/gateway/src/components/pricing-table.tsx
index e9bc31b24..f2d76bbe3 100644
--- a/packages/billing/gateway/src/components/pricing-table.tsx
+++ b/packages/billing/gateway/src/components/pricing-table.tsx
@@ -161,7 +161,7 @@ function PricingItem(
data-cy={'subscription-plan'}
className={cn(
props.className,
- `s-full relative flex flex-1 grow flex-col items-stretch justify-between
+ `s-full relative flex flex-1 grow flex-col items-stretch justify-between
self-stretch rounded-lg border p-8 lg:w-4/12 xl:max-w-[20rem]`,
{
['border-primary']: highlighted,
@@ -210,11 +210,9 @@ function PricingItem(
- {lineItem ? (
- formatCurrency(props.product.currency, lineItem.cost)
- ) : (
-
- )}
+ {lineItem
+ ? formatCurrency(props.product.currency, lineItem.cost)
+ : props.plan.label ?? }
@@ -415,7 +413,7 @@ function DefaultCheckoutButton(
id: string;
name?: string | undefined;
href?: string;
- label?: string;
+ buttonLabel?: string;
};
product: {
@@ -443,7 +441,7 @@ function DefaultCheckoutButton(
`${signUpPath}?plan=${planId}&next=${subscriptionPath}?plan=${planId}${redirectToCheckoutParam}` ??
'';
- const label = props.plan.label ?? 'common:getStartedWithPlan';
+ const label = props.plan.buttonLabel ?? 'common:getStartedWithPlan';
return (
diff --git a/packages/features/team-accounts/src/components/create-team-account-dialog.tsx b/packages/features/team-accounts/src/components/create-team-account-dialog.tsx
index 41f88b430..5bf99a500 100644
--- a/packages/features/team-accounts/src/components/create-team-account-dialog.tsx
+++ b/packages/features/team-accounts/src/components/create-team-account-dialog.tsx
@@ -1,3 +1,5 @@
+'use client';
+
import { useState, useTransition } from 'react';
import { zodResolver } from '@hookform/resolvers/zod';
diff --git a/packages/features/team-accounts/src/components/index.ts b/packages/features/team-accounts/src/components/index.ts
index bf908ef14..3ed2dd258 100644
--- a/packages/features/team-accounts/src/components/index.ts
+++ b/packages/features/team-accounts/src/components/index.ts
@@ -4,3 +4,4 @@ export * from './settings/team-account-danger-zone';
export * from './invitations/account-invitations-table';
export * from './settings/team-account-settings-container';
export * from './invitations/accept-invitation-container';
+export * from './create-team-account-dialog';
diff --git a/packages/ui/package.json b/packages/ui/package.json
index cb2d87014..e0669eed9 100644
--- a/packages/ui/package.json
+++ b/packages/ui/package.json
@@ -117,7 +117,8 @@
"./enhanced-data-table": "./src/makerkit/data-table.tsx",
"./language-selector": "./src/makerkit/language-selector.tsx",
"./stepper": "./src/makerkit/stepper.tsx",
- "./cookie-banner": "./src/makerkit/cookie-banner.tsx"
+ "./cookie-banner": "./src/makerkit/cookie-banner.tsx",
+ "./card-button": "./src/makerkit/card-button.tsx"
},
"typesVersions": {
"*": {
diff --git a/packages/ui/src/makerkit/bordered-navigation-menu.tsx b/packages/ui/src/makerkit/bordered-navigation-menu.tsx
index a81ffb16d..718dacb37 100644
--- a/packages/ui/src/makerkit/bordered-navigation-menu.tsx
+++ b/packages/ui/src/makerkit/bordered-navigation-menu.tsx
@@ -9,6 +9,7 @@ import {
NavigationMenuItem,
NavigationMenuList,
} from '../shadcn/navigation-menu';
+
import { cn, isRouteActive } from '../utils';
import { Trans } from './trans';
@@ -25,10 +26,14 @@ export function BorderedNavigationMenu(props: React.PropsWithChildren) {
export function BorderedNavigationMenuItem(props: {
path: string;
label: string;
+ end?: boolean | ((path: string) => boolean);
active?: boolean;
}) {
const pathname = usePathname();
- const active = props.active ?? isRouteActive(pathname, props.path);
+
+ const active =
+ props.active ??
+ isRouteActive(props.path, pathname, props.end);
return (
@@ -47,4 +52,4 @@ export function BorderedNavigationMenuItem(props: {
);
-}
+}
\ No newline at end of file
diff --git a/packages/ui/src/makerkit/card-button.tsx b/packages/ui/src/makerkit/card-button.tsx
new file mode 100644
index 000000000..5f80724e6
--- /dev/null
+++ b/packages/ui/src/makerkit/card-button.tsx
@@ -0,0 +1,46 @@
+import * as React from 'react';
+
+import { Slot, Slottable } from '@radix-ui/react-slot';
+import { ChevronRight } from 'lucide-react';
+
+import { cn } from '../utils';
+
+export const CardButton = React.forwardRef<
+ HTMLButtonElement,
+ {
+ asChild?: boolean;
+ className?: string;
+ children: React.ReactNode;
+ }
+>(({ className, asChild, ...props }, ref) => {
+ const Comp = asChild ? Slot : 'button';
+
+ return (
+
+ {props.children}
+
+ );
+});
+
+export function CardButtonHeader(props: React.PropsWithChildren) {
+ return (
+ <>
+
+ {props.children}
+
+
+
+ >
+ );
+}
diff --git a/packages/ui/src/makerkit/navigation-config.schema.ts b/packages/ui/src/makerkit/navigation-config.schema.ts
index d49e5ab7a..ba913ba9c 100644
--- a/packages/ui/src/makerkit/navigation-config.schema.ts
+++ b/packages/ui/src/makerkit/navigation-config.schema.ts
@@ -1,5 +1,10 @@
import { z } from 'zod';
+const RouteMatchingEnd = z
+ .union([z.boolean(), z.function().args(z.string()).returns(z.boolean())])
+ .default(false)
+ .optional();
+
export const NavigationConfigSchema = z.object({
style: z.enum(['custom', 'sidebar', 'header']).default('sidebar'),
routes: z.array(
@@ -8,7 +13,7 @@ export const NavigationConfigSchema = z.object({
label: z.string(),
path: z.string(),
Icon: z.custom(),
- end: z.boolean().optional(),
+ end: RouteMatchingEnd,
}),
z.object({
label: z.string(),
@@ -19,7 +24,7 @@ export const NavigationConfigSchema = z.object({
label: z.string(),
path: z.string(),
Icon: z.custom(),
- end: z.boolean().optional(),
+ end: RouteMatchingEnd,
}),
),
}),
diff --git a/packages/ui/src/makerkit/sidebar.tsx b/packages/ui/src/makerkit/sidebar.tsx
index 0893b01da..a15405fe4 100644
--- a/packages/ui/src/makerkit/sidebar.tsx
+++ b/packages/ui/src/makerkit/sidebar.tsx
@@ -154,12 +154,12 @@ export function SidebarItem({
}: React.PropsWithChildren<{
path: string;
Icon: React.ReactNode;
- end?: boolean;
+ end?: boolean | ((path: string) => boolean);
}>) {
const { collapsed } = useContext(SidebarContext);
-
const currentPath = usePathname() ?? '';
- const active = isRouteActive(path, currentPath, end ? 0 : 3);
+
+ const active = isRouteActive(path, currentPath, end);
const variant = active ? 'secondary' : 'ghost';
const size = collapsed ? 'icon' : 'sm';
@@ -254,4 +254,4 @@ export function SidebarNavigation({
})}
>
);
-}
+}
\ No newline at end of file
diff --git a/packages/ui/src/utils/is-route-active.ts b/packages/ui/src/utils/is-route-active.ts
index 06fb745bb..accd69046 100644
--- a/packages/ui/src/utils/is-route-active.ts
+++ b/packages/ui/src/utils/is-route-active.ts
@@ -3,12 +3,45 @@ const ROOT_PATH = '/';
/**
* @name isRouteActive
* @description A function to check if a route is active. This is used to
+ * @param end
+ * @param path
+ * @param currentPath
+ */
+export function isRouteActive(
+ path: string,
+ currentPath: string,
+ end?: boolean | ((path: string) => boolean) | undefined,
+) {
+ // if the path is the same as the current path, we return true
+ if (path === currentPath) {
+ return true;
+ }
+
+ // if the end prop is a function, we call it with the current path
+ if (typeof end === 'function') {
+ return !end(currentPath);
+ }
+
+ // otherwise - we use the evaluateIsRouteActive function
+ const defaultEnd = end ?? true;
+ const oneLevelDeep = 1;
+ const threeLevelsDeep = 3;
+
+ // how far down should segments be matched?
+ const depth = defaultEnd ? oneLevelDeep : threeLevelsDeep;
+
+ return checkIfRouteIsActive(path, currentPath, depth);
+}
+
+/**
+ * @name checkIfRouteIsActive
+ * @description A function to check if a route is active. This is used to
* highlight the active link in the navigation.
* @param targetLink - The link to check against
* @param currentRoute - the current route
* @param depth - how far down should segments be matched?
*/
-export function isRouteActive(
+export function checkIfRouteIsActive(
targetLink: string,
currentRoute: string,
depth = 1,