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,