From fc2f817af9cefb6152780178ba3d35e6314dda43 Mon Sep 17 00:00:00 2001 From: Developing-Gamer Date: Wed, 15 Jul 2026 19:29:49 -0700 Subject: [PATCH] More design improvements --- .../products/[productId]/page-client.tsx | 325 ++++++++++++------ .../design-components/editable-grid.tsx | 82 ++++- .../src/components/design-components/index.ts | 13 +- 3 files changed, 305 insertions(+), 115 deletions(-) diff --git a/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/[productId]/page-client.tsx b/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/[productId]/page-client.tsx index 1d70528ed..6700371be 100644 --- a/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/[productId]/page-client.tsx +++ b/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/[productId]/page-client.tsx @@ -1,13 +1,15 @@ "use client"; import { + DesignBadge, DesignEditableGrid, DesignButton, DesignInput, + DesignMenu, DesignSelectorDropdown, + type DesignBadgeColor, type DesignEditableGridItem, } from "@/components/design-components"; -import { EditableInput } from "@/components/editable-input"; import { Link, StyledLink } from "@/components/link"; import { ItemDialog } from "@/components/payments/item-dialog"; import { useRouter } from "@/components/router"; @@ -26,10 +28,6 @@ import { DialogFooter, DialogHeader, DialogTitle, - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuTrigger, Input, Label, Select, @@ -45,7 +43,8 @@ import { } from "@/components/ui"; import { createDefaultDataGridState, DataGrid, useDataGridUrlState, useDataSource, type DataGridColumnDef } from "@hexclave/dashboard-ui-components"; import { useUpdateConfig } from "@/components/config-update"; -import { ArrowLeftIcon, ClockIcon, CopyIcon, CurrencyDollarIcon, DotsThreeIcon, FolderOpenIcon, GiftIcon, HardDriveIcon, PackageIcon, PencilSimpleIcon, PlusIcon, PuzzlePieceIcon, ShoppingCartIcon, StackIcon, TagIcon, TrashIcon, UsersIcon } from "@phosphor-icons/react"; +import { ArrowLeftIcon, Check, ClockIcon, CopyIcon, CurrencyDollarIcon, FolderOpenIcon, GiftIcon, HardDriveIcon, PackageIcon, PencilSimpleIcon, PlusIcon, PuzzlePieceIcon, ShoppingCartIcon, StackIcon, TagIcon, TrashIcon, UsersIcon, X } from "@phosphor-icons/react"; +import { runAsynchronouslyWithAlert } from "@hexclave/shared/dist/utils/promises"; import { CreateCheckoutDialog } from "@/components/payments/create-checkout-dialog"; import type { CustomerType } from "@/components/payments/customer-selector"; import type { CompleteConfig } from "@hexclave/shared/dist/config/schema"; @@ -54,8 +53,7 @@ import type { DayInterval } from "@hexclave/shared/dist/utils/dates"; import { fromNow } from "@hexclave/shared/dist/utils/dates"; import { prettyPrintWithMagnitudes } from "@hexclave/shared/dist/utils/numbers"; import { typedEntries } from "@hexclave/shared/dist/utils/objects"; -import { runAsynchronouslyWithAlert } from "@hexclave/shared/dist/utils/promises"; -import { Suspense, useMemo, useState } from "react"; +import { Suspense, useMemo, useRef, useState } from "react"; import { PageLayout } from "../../../page-layout"; import { useAdminApp, useProjectId } from "../../../use-admin-app"; import { CreateProductLineDialog } from "../create-product-line-dialog"; @@ -74,6 +72,16 @@ const CUSTOMER_TYPE_COLORS = { custom: 'bg-amber-500/15 text-amber-600 dark:bg-amber-500/20 dark:text-amber-400 ring-amber-500/30', } as const; +const CUSTOMER_TYPE_BADGE_COLORS = new Map([ + ["user", "blue"], + ["team", "green"], + ["custom", "orange"], +]); + +function getCustomerTypeBadgeColor(customerType: string): DesignBadgeColor { + return CUSTOMER_TYPE_BADGE_COLORS.get(customerType) ?? "blue"; +} + export default function PageClient({ productId }: { productId: string }) { const adminApp = useAdminApp(); const project = adminApp.useProject(); @@ -116,15 +124,15 @@ function ProductPage({ productId, product, config }: ProductPageProps) {
{canGoBack && ( - + )} @@ -144,119 +152,240 @@ type ProductHeaderProps = { productLineName: string | null, }; +function ProductTitleEditor({ + productId, + displayName, + storedDisplayName, +}: { + productId: string, + displayName: string, + storedDisplayName: string, +}) { + const adminApp = useAdminApp(); + const updateConfig = useUpdateConfig(); + const [editing, setEditing] = useState(false); + const [draftValue, setDraftValue] = useState(storedDisplayName); + const [isSaving, setIsSaving] = useState(false); + const inputRef = useRef(null); + + const beginEditing = () => { + setDraftValue(storedDisplayName); + setEditing(true); + }; + + const cancelEditing = () => { + setDraftValue(storedDisplayName); + setEditing(false); + }; + + const save = async () => { + setIsSaving(true); + try { + const success = await updateConfig({ + adminApp, + configUpdate: { + [`payments.products.${productId}.displayName`]: draftValue || null, + }, + pushable: true, + }); + if (success) { + toast({ title: "Product name updated" }); + setEditing(false); + } + } finally { + setIsSaving(false); + } + }; + + return ( +
{ + if (!editing) { + beginEditing(); + } else { + inputRef.current?.focus(); + } + }} + onKeyDown={(event) => { + if (editing) { + return; + } + if (event.key === "Enter" || event.key === " ") { + event.preventDefault(); + beginEditing(); + } + }} + > + {editing ? ( + event.stopPropagation()} + onChange={(event) => setDraftValue(event.target.value)} + onKeyDown={(event) => { + if (event.key === "Enter" && !event.nativeEvent.isComposing) { + event.preventDefault(); + runAsynchronouslyWithAlert(save()); + } + if (event.key === "Escape") { + cancelEditing(); + } + }} + className="min-w-0 flex-1 bg-transparent text-lg font-semibold leading-none tracking-tight text-foreground outline-none placeholder:text-muted-foreground/60 disabled:opacity-50" + /> + ) : ( +

+ {displayName} +

+ )} + {editing ? ( +
event.stopPropagation()}> + + + + + + +
+ ) : ( + + )} +
+ ); +} + function ProductHeader({ productId, product, productLineName }: ProductHeaderProps) { const projectId = useProjectId(); const adminApp = useAdminApp(); const project = adminApp.useProject(); const config = project.useConfig(); - const updateConfig = useUpdateConfig(); const router = useRouter(); const [isCheckoutOpen, setIsCheckoutOpen] = useState(false); const displayName = product.displayName || productId; - const isAddOn = product.isAddOnTo !== false && typeof product.isAddOnTo === 'object'; + const isAddOn = product.isAddOnTo !== false && typeof product.isAddOnTo === "object"; - // Find add-on parent products const addOnParents = useMemo(() => { - if (product.isAddOnTo === false || typeof product.isAddOnTo !== 'object') return []; - return Object.keys(product.isAddOnTo).map((parentId: string) => ({ + if (product.isAddOnTo === false || typeof product.isAddOnTo !== "object") { + return []; + } + return Object.keys(product.isAddOnTo).map((parentId) => ({ id: parentId, displayName: config.payments.products[parentId].displayName || parentId, })); }, [product.isAddOnTo, config.payments.products]); return ( -
-
+
+
{isAddOn ? ( - + ) : ( - + )}
-
+ +
-
- { - const success = await updateConfig({ adminApp, configUpdate: { - [`payments.products.${productId}.displayName`]: newName || null, - }, pushable: true }); - if (success) { - toast({ title: "Product name updated" }); - } - }} + +
+ + , + onClick: () => setIsCheckoutOpen(true), + }, + { + id: "product-lines", + label: "View in Product Lines", + icon: , + onClick: () => router.push(`/projects/${projectId}/payments/product-lines#product-${productId}`), + }, + ]} />
- - - - - - - - } onClick={() => setIsCheckoutOpen(true)}> - Create checkout - - router.push(`/projects/${projectId}/payments/product-lines#product-${productId}`)}> - View in Product Lines - - -
-
- - {product.customerType} + +
+ + + ID: {productId} - ID: {productId} - {productLineName && ( - <> - - Product Line: {productLineName} - + {productLineName != null && ( + + · + Product Line: {productLineName} + )} {addOnParents.length > 0 && ( - <> - - - Add-on to{' '} - {addOnParents.map((p: { id: string, displayName: string }, i: number) => ( - - {i > 0 && ", "} - - {p.displayName} - - - ))} - - + + · + Add-on to{" "} + {addOnParents.map((parent, index) => ( + + {index > 0 && ,} + + {parent.displayName} + + + ))} + )}
diff --git a/apps/dashboard/src/components/design-components/editable-grid.tsx b/apps/dashboard/src/components/design-components/editable-grid.tsx index 6261283d8..54955c6e6 100644 --- a/apps/dashboard/src/components/design-components/editable-grid.tsx +++ b/apps/dashboard/src/components/design-components/editable-grid.tsx @@ -1,6 +1,7 @@ "use client"; import { + Checkbox, Popover, PopoverContent, PopoverTrigger, @@ -348,6 +349,71 @@ function EditableTextField({ ); } +function EditableBooleanField({ + item, + editMode, + size, +}: { + item: BooleanItem, + editMode: boolean, + size: FieldSizeConfig, +}) { + const [isUpdating, setIsUpdating] = useState(false); + const canEdit = item.readOnly !== true && item.onUpdate != null; + const trueLabel = item.trueLabel ?? "Yes"; + const falseLabel = item.falseLabel ?? "No"; + const statusLabel = item.value ? trueLabel : falseLabel; + + if (!canEdit) { + return ( +
+ {statusLabel} +
+ ); + } + + const updateValue = async (nextValue: boolean) => { + if (item.onUpdate == null) { + throw new Error(`Editable boolean item "${item.name}" requires an onUpdate handler.`); + } + setIsUpdating(true); + try { + await item.onUpdate(nextValue); + } finally { + setIsUpdating(false); + } + }; + + return ( + + ); +} + function AsyncSelectField({ value, options, @@ -510,21 +576,7 @@ function ItemValue({ return ; } case "boolean": { - const onUpdate = item.onUpdate; - return ( - await onUpdate(value === "true")} - options={[ - { value: "true", label: item.trueLabel ?? "Yes" }, - { value: "false", label: item.falseLabel ?? "No" }, - ]} - readOnly={item.readOnly} - size={size} - value={item.value ? "true" : "false"} - /> - ); + return ; } case "dropdown": { return ( diff --git a/apps/dashboard/src/components/design-components/index.ts b/apps/dashboard/src/components/design-components/index.ts index c62fb9f3a..54cd784d0 100644 --- a/apps/dashboard/src/components/design-components/index.ts +++ b/apps/dashboard/src/components/design-components/index.ts @@ -14,8 +14,17 @@ export type { } from "../../../../../packages/dashboard-ui-components/src/components/dialog"; export * from "./analytics-card"; export * from "./design-tokens"; -export * from "./editable-grid.tsx"; -export type * from "./editable-grid.tsx"; +export { + DesignEditableGrid, + designEditableGridControlClassName, + designEditableGridPopoverClassName, +} from "./editable-grid"; +export type { + DesignEditableGridDropdownOption, + DesignEditableGridItem, + DesignEditableGridProps, + DesignEditableGridSize, +} from "./editable-grid"; export * from "./list"; export * from "./menu"; export * from "./select";