From 84271a31a8bb02de8a038f2bc494d2783da8a3a2 Mon Sep 17 00:00:00 2001 From: giancarlo Date: Mon, 13 May 2024 02:09:28 +0700 Subject: [PATCH] Add card button component and enhance routing This commit introduces the Card Button component to the UI package and improves URL matching by allowing end to be a boolean or function. Navigation menu components now support both cases. Additionally, changes have been made to improve the visual and functional aspects of various components throughout the application. This includes changes in pricing display, route active checking, and the introduction of new files such as HomeAddAccountButton and HomeAccountsList. --- .../docs/_components/docs-navigation.tsx | 2 +- .../(user)/_components/home-accounts-list.tsx | 50 +++++++++++++++++++ .../_components/home-add-account-button.tsx | 23 +++++++++ .../_components/home-menu-navigation.tsx | 2 +- .../team-account-navigation-menu.tsx | 2 +- .../billing/core/src/create-billing-schema.ts | 1 + .../gateway/src/components/pricing-table.tsx | 14 +++--- .../components/create-team-account-dialog.tsx | 2 + .../team-accounts/src/components/index.ts | 1 + packages/ui/package.json | 3 +- .../src/makerkit/bordered-navigation-menu.tsx | 9 +++- packages/ui/src/makerkit/card-button.tsx | 46 +++++++++++++++++ .../src/makerkit/navigation-config.schema.ts | 9 +++- packages/ui/src/makerkit/sidebar.tsx | 8 +-- packages/ui/src/utils/is-route-active.ts | 35 ++++++++++++- 15 files changed, 186 insertions(+), 21 deletions(-) create mode 100644 apps/web/app/home/(user)/_components/home-accounts-list.tsx create mode 100644 apps/web/app/home/(user)/_components/home-add-account-button.tsx create mode 100644 packages/ui/src/makerkit/card-button.tsx 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,