diff --git a/app/components/layout/sidebar/sidebar.tsx b/app/components/layout/sidebar/sidebar.tsx
index 758d85338..448634d96 100644
--- a/app/components/layout/sidebar/sidebar.tsx
+++ b/app/components/layout/sidebar/sidebar.tsx
@@ -12,6 +12,7 @@ import { useFetcher } from "@remix-run/react";
import type { VariantProps } from "class-variance-authority";
import { cva } from "class-variance-authority";
+import { MenuIcon } from "lucide-react";
import Input from "~/components/forms/input";
import { SwitchIcon } from "~/components/icons/library";
import { Button } from "~/components/shared/button";
@@ -306,7 +307,12 @@ const SidebarTrigger = forwardRef<
}}
{...props}
>
-
+
+
Toggle Sidebar
);
diff --git a/app/components/shared/button.tsx b/app/components/shared/button.tsx
index 8aa4dde7b..9c3d23e16 100644
--- a/app/components/shared/button.tsx
+++ b/app/components/shared/button.tsx
@@ -1,6 +1,6 @@
import React from "react";
import { ExternalLinkIcon } from "@radix-ui/react-icons";
-import { Link } from "@remix-run/react";
+import { Link, type LinkProps } from "@remix-run/react";
import { tw } from "~/utils/tw";
import { HoverCard, HoverCardContent, HoverCardTrigger } from "./hover-card";
import type { IconType } from "./icons-map";
@@ -8,8 +8,7 @@ import Icon from "../icons/icon";
import type { ButtonVariant, ButtonWidth } from "../layout/header/types";
/**
- * Type for the disabled prop
- * We export it as its being used in other places as well
+ * Type for the disabled prop that can either be a boolean or an object with additional info
*/
export type DisabledProp =
| boolean
@@ -18,26 +17,134 @@ export type DisabledProp =
reason: React.ReactNode | string;
};
-export interface ButtonProps {
- as?: React.ComponentType | string;
+/**
+ * Defines valid button size options
+ */
+export type ButtonSize = "xs" | "sm" | "md";
+/**
+ * Common button props that all button types share
+ */
+export interface CommonButtonProps {
className?: string;
variant?: ButtonVariant;
width?: ButtonWidth;
- size?: "xs" | "sm" | "md";
+ size?: ButtonSize;
icon?: IconType;
- /** Disabled can be a boolean */
- disabled?: DisabledProp;
-
attachToInput?: boolean;
onlyIconOnMobile?: boolean;
- title?: string;
+ error?: string;
+ hideErrorText?: boolean;
+ children?: React.ReactNode;
+ disabled?: DisabledProp;
+ label?: string; // Add label here since it's used in BookLink
+ id?: string; // Add id as an optional prop since some buttons might need it
+}
+
+/**
+ * Props specific to HTML button elements
+ */
+export interface HTMLButtonProps
+ extends Omit,
+ Omit<
+ React.ButtonHTMLAttributes,
+ keyof CommonButtonProps | "disabled"
+ > {
+ as?: "button";
+ to?: never;
+ disabled?: DisabledProp;
+}
+
+/**
+ * Props specific to Link components
+ */
+export interface LinkButtonProps
+ extends CommonButtonProps,
+ Omit {
+ as?: typeof Link;
+ to: string;
+ target?: string;
prefetch?: "none" | "intent" | "render" | "viewport";
+}
+
+/**
+ * Props for custom component buttons
+ */
+export interface CustomComponentButtonProps extends CommonButtonProps {
+ as: React.ComponentType;
[key: string]: any;
}
+/**
+ * Union type of all possible button prop combinations
+ */
+export type ButtonProps =
+ | HTMLButtonProps
+ | LinkButtonProps
+ | CustomComponentButtonProps;
+/**
+ * Type guard to check if props are for a Link button
+ */
+function isLinkProps(props: ButtonProps): props is LinkButtonProps {
+ return "to" in props;
+}
+
+/**
+ * Style mappings for button variants
+ */
+const variants: Record = {
+ primary: tw(
+ `border-primary-400 bg-primary-500 text-white focus:ring-2`,
+ "disabled:border-primary-300 disabled:bg-primary-300",
+ "enabled:hover:bg-primary-400"
+ ),
+ secondary: tw(
+ `border-gray-300 bg-white text-gray-700`,
+ "disabled:text-gray-500",
+ "enabled:hover:bg-gray-50"
+ ),
+ tertiary: tw(
+ `border-b border-primary/10 pb-1 leading-none`,
+ "disabled:text-gray-300"
+ ),
+ link: tw(
+ `border-none p-0 text-text-sm font-semibold text-primary-700 hover:text-primary-800`
+ ),
+ "block-link": tw(
+ "-mt-1 border-none px-2 py-1 text-[14px] font-normal hover:bg-primary-50 hover:text-primary-600"
+ ),
+ "block-link-gray": tw(
+ "-mt-1 border-none px-2 py-1 text-[14px] font-normal hover:bg-gray-50 hover:text-gray-600"
+ ),
+ danger: tw(
+ `border-error-600 bg-error-600 text-white focus:ring-2`,
+ "disabled:border-error-300 disabled:bg-error-300",
+ "enabled:hover:bg-error-800"
+ ),
+};
+
+/**
+ * Style mappings for button sizes
+ */
+const sizes: Record = {
+ xs: tw("px-2 py-[6px] text-xs"),
+ sm: tw("px-[14px] py-2"),
+ md: tw("px-4 py-[10px]"),
+};
+
+/**
+ * Style mappings for button widths
+ */
+const widths: Record = {
+ auto: "w-auto",
+ full: "w-full max-w-full",
+};
+
+/**
+ * Button component that supports multiple variants, sizes, and can render as different elements
+ */
export const Button = React.forwardRef(
- function Button(props: ButtonProps, ref) {
- let {
+ function Button(
+ {
as = "button",
className,
variant = "primary",
@@ -50,145 +157,91 @@ export const Button = React.forwardRef(
onlyIconOnMobile,
error,
hideErrorText = false,
- target,
- } = props;
- const Component: React.ComponentType | string = props?.to ? Link : as;
- const baseButtonClasses = `inline-flex items-center justify-center rounded font-semibold text-center gap-2 max-w-xl border text-sm box-shadow-xs`;
-
- const variants = {
- primary: tw(
- `border-primary-400 bg-primary-500 text-white focus:ring-2`,
- disabled ? "border-primary-300 bg-primary-300" : "hover:bg-primary-400"
- ),
- secondary: tw(
- `border-gray-300 bg-white text-gray-700 `,
- disabled ? "text-gray-500" : "hover:bg-gray-50"
- ),
- tertiary: tw(
- `border-b border-primary/10 pb-1 leading-none`,
- disabled ? "text-gray-300" : ""
- ),
- link: tw(
- `border-none p-0 text-text-sm font-semibold text-primary-700 hover:text-primary-800`
- ),
- "block-link": tw(
- "-mt-1 border-none px-2 py-1 text-[14px] font-normal hover:bg-primary-50 hover:text-primary-600"
- ),
-
- "block-link-gray": tw(
- "-mt-1 border-none px-2 py-1 text-[14px] font-normal hover:bg-gray-50 hover:text-gray-600"
- ),
- danger: tw(
- `border-error-600 bg-error-600 text-white focus:ring-2`,
- disabled ? "border-error-300 bg-error-300" : "hover:bg-error-800"
- ),
- };
+ ...props
+ },
+ ref
+ ) {
+ const Component = isLinkProps(props) ? Link : as;
+ const baseButtonClasses =
+ "inline-flex items-center justify-center rounded font-semibold text-center gap-2 max-w-xl border text-sm box-shadow-xs";
- const sizes = {
- xs: tw("px-2 py-[6px] text-xs"),
- sm: tw("px-[14px] py-2"),
- md: tw("px-4 py-[10px]"),
- };
+ const isDisabled =
+ typeof disabled === "boolean" ? disabled : disabled !== undefined;
+ const disabledReason =
+ typeof disabled === "object" ? disabled.reason : undefined;
+ const disabledTitle =
+ typeof disabled === "object" ? disabled.title : undefined;
- const widths = {
- auto: "w-auto",
- full: "w-full max-w-full",
- };
+ // Type guard for checking if props has target property
+ const hasTarget = (props: ButtonProps): props is LinkButtonProps =>
+ "target" in props;
+ const newTab = hasTarget(props) && props.target === "_blank";
- const disabledStyles = disabled
- ? "opacity-50 cursor-not-allowed"
- : undefined;
- const attachedStyles = attachToInput
- ? tw(" rounded-l-none border-l-0")
- : undefined;
+ const buttonContent = (
+ <>
+ {icon && }
+ {children && (
+
+ {children}
+ {newTab && (
+
+ )}
+
+ )}
+ >
+ );
const finalStyles = tw(
baseButtonClasses,
- sizes[size],
- variants[variant],
- widths[width],
- disabledStyles,
- attachedStyles,
- className,
- error
- ? "border-error-300 focus:border-error-300 focus:ring-error-100"
- : ""
+ sizes[size as ButtonSize], // Type assertion to ensure correct indexing
+ variants[variant as ButtonVariant],
+ widths[width as ButtonWidth],
+ isDisabled && "cursor-not-allowed opacity-50",
+ attachToInput && "rounded-l-none border-l-0",
+ error && "border-error-300 focus:border-error-300 focus:ring-error-100",
+ className
);
- const isDisabled =
- disabled === undefined // If it is undefined, then it is not disabled
- ? false
- : typeof disabled === "boolean"
- ? disabled
- : true; // If it is an object, then it is disabled
- const reason = typeof disabled === "object" ? disabled.reason : "";
- const disabledTitle =
- typeof disabled === "object" ? disabled.title : undefined;
-
- const newTab = target === "_blank";
-
- if (isDisabled) {
+ // Render disabled button with hover card
+ if (isDisabled && disabledReason) {
return (
-
+
) => {
- e.preventDefault();
- }}
- onClick={(e: React.MouseEvent) => {
- e.preventDefault();
- }}
+ onMouseDown={(e: React.MouseEvent) => e.preventDefault()}
+ onClick={(e: React.MouseEvent) => e.preventDefault()}
>
- {icon && }{" "}
- {children ? (
-
- {children}
-
- ) : null}
+ {buttonContent}
- {reason && (
-
-
- {typeof disabledStyles === "string"
- ? disabledTitle
- : "Action disabled"}
-
- {reason}
-
- )}
+
+
+ {disabledTitle || "Action disabled"}
+
+ {disabledReason}
+
);
}
+
+ // Render normal button
return (
<>
- {icon && }{" "}
- {children ? (
-
- {children}{" "}
- {newTab && (
-
- )}
-
- ) : null}
+ {buttonContent}
{!hideErrorText && error && (
{error}
@@ -197,3 +250,5 @@ export const Button = React.forwardRef(
);
}
);
+
+Button.displayName = "Button";
diff --git a/app/components/shared/generic-add-to-bookings-actions-dropdown.tsx b/app/components/shared/generic-add-to-bookings-actions-dropdown.tsx
index 626c65cab..9f26c6c44 100644
--- a/app/components/shared/generic-add-to-bookings-actions-dropdown.tsx
+++ b/app/components/shared/generic-add-to-bookings-actions-dropdown.tsx
@@ -1,7 +1,10 @@
import { ChevronRightIcon } from "@radix-ui/react-icons";
import { useHydrated } from "remix-utils/use-hydrated";
import Icon from "~/components/icons/icon";
-import type { ButtonProps, DisabledProp } from "~/components/shared/button";
+import type {
+ CommonButtonProps,
+ DisabledProp,
+} from "~/components/shared/button";
import { Button } from "~/components/shared/button";
import {
@@ -17,16 +20,21 @@ import When from "../when/when";
type IndexType = "kit" | "asset";
-export interface BookLink extends ButtonProps {
+/**
+ * Extend CommonButtonProps instead of ButtonProps for the interface
+ */
+export interface BookLink extends CommonButtonProps {
indexType: IndexType;
+ to: string; // Add required properties from LinkButtonProps
+ id: string; // Make id required for BookLink
+ icon?: "bookings" | "booking-exist"; // Narrow down the icon types
}
-
const ConditionalActionsDropdown = ({
links,
label,
disabledTrigger,
}: {
- links: ButtonProps[];
+ links: BookLink[];
label: string;
disabledTrigger?: DisabledProp;
diff --git a/app/components/table.tsx b/app/components/table.tsx
index 9ffe7ec51..c314f5448 100644
--- a/app/components/table.tsx
+++ b/app/components/table.tsx
@@ -27,7 +27,7 @@ export function Table({