@tuturuuu/ui 0.8.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +40 -0
- package/biome.json +1 -1
- package/package.json +73 -71
- package/src/components/ui/accordion.tsx +1 -1
- package/src/components/ui/breadcrumb.tsx +1 -1
- package/src/components/ui/calendar-app/calendar-page-shell.tsx +4 -0
- package/src/components/ui/calendar-app/components/calendar-connections-settings-content.tsx +239 -33
- package/src/components/ui/calendar-app/components/load-smart-scheduling-tasks.tsx +143 -0
- package/src/components/ui/calendar-app/components/priority-view.tsx +10 -3
- package/src/components/ui/calendar-app/components/tasks-sidebar.tsx +4 -116
- package/src/components/ui/calendar-app/components/use-calendar-connections-manager.ts +67 -2
- package/src/components/ui/calendar.tsx +1 -1
- package/src/components/ui/carousel.tsx +1 -1
- package/src/components/ui/chat/chat-agent-details-external-thread-panel.test.tsx +1 -1
- package/src/components/ui/chat/chat-agent-details-external-thread-panel.tsx +1 -1
- package/src/components/ui/chat/chat-agent-details-operations-panel.test.tsx +1 -1
- package/src/components/ui/chat/chat-agent-details-operations-panel.tsx +1 -1
- package/src/components/ui/chat/chat-agent-details-setup-panel.tsx +1 -1
- package/src/components/ui/chat/chat-agent-details-sidebar.test.tsx +1 -1
- package/src/components/ui/chat/chat-agent-details-sidebar.tsx +2 -2
- package/src/components/ui/chat/chat-agent-details-utils.test.ts +1 -1
- package/src/components/ui/chat/chat-agent-details-utils.tsx +1 -1
- package/src/components/ui/chat/chat-agent-details-zalo-personal-panel.tsx +2 -2
- package/src/components/ui/checkbox.tsx +1 -1
- package/src/components/ui/color-picker.tsx +1 -1
- package/src/components/ui/command.tsx +1 -1
- package/src/components/ui/context-menu.tsx +5 -1
- package/src/components/ui/custom/__tests__/settings-dialog-shell.test.tsx +3 -0
- package/src/components/ui/custom/__tests__/workspace-select-helpers.test.ts +19 -0
- package/src/components/ui/custom/combobox.test.tsx +195 -0
- package/src/components/ui/custom/combobox.tsx +273 -156
- package/src/components/ui/custom/education/modules/youtube/delete-link-button.tsx +5 -13
- package/src/components/ui/custom/facebook-mockup/facebook-mockup.tsx +7 -1
- package/src/components/ui/custom/facebook-mockup/form.tsx +1 -1
- package/src/components/ui/custom/facebook-mockup/image-upload-field.tsx +1 -1
- package/src/components/ui/custom/facebook-mockup/preview.tsx +1 -1
- package/src/components/ui/custom/settings-dialog-shell.tsx +2 -1
- package/src/components/ui/custom/theme-toggle.tsx +1 -1
- package/src/components/ui/custom/workspace-select.tsx +8 -3
- package/src/components/ui/dialog.test.tsx +52 -0
- package/src/components/ui/dialog.tsx +6 -2
- package/src/components/ui/dropdown-menu.tsx +5 -1
- package/src/components/ui/finance/debts/debt-loan-form.tsx +12 -5
- package/src/components/ui/finance/debts/debt-loan-summary.tsx +3 -2
- package/src/components/ui/finance/debts/debts-page.test.tsx +54 -5
- package/src/components/ui/finance/debts/debts-page.tsx +15 -2
- package/src/components/ui/finance/invoices/components/subscription-group-selector.tsx +3 -5
- package/src/components/ui/finance/invoices/new-invoice-page.test.tsx +25 -5
- package/src/components/ui/finance/invoices/new-invoice-page.tsx +7 -2
- package/src/components/ui/finance/invoices/standard-invoice.tsx +4 -2
- package/src/components/ui/finance/invoices/subscription-invoice.tsx +4 -2
- package/src/components/ui/finance/invoices/utils.ts +3 -1
- package/src/components/ui/finance/transactions/form-content-dialog.tsx +3 -0
- package/src/components/ui/finance/transactions/form-types.ts +1 -0
- package/src/components/ui/finance/transactions/form.tsx +2 -0
- package/src/components/ui/finance/transactions/infinite-transactions-list.tsx +2 -0
- package/src/components/ui/finance/transactions/period-charts/category-breakdown-dialog.tsx +1 -1
- package/src/components/ui/finance/transactions/transaction-edit-dialog.tsx +1 -4
- package/src/components/ui/finance/transactions/transactions-create-summary.tsx +3 -0
- package/src/components/ui/finance/transactions/transactions-page.tsx +4 -1
- package/src/components/ui/finance/wallets/form.test.tsx +51 -3
- package/src/components/ui/finance/wallets/form.tsx +15 -4
- package/src/components/ui/finance/wallets/walletId/wallet-details-actions.tsx +4 -0
- package/src/components/ui/finance/wallets/walletId/wallet-details-page.tsx +4 -2
- package/src/components/ui/finance/wallets/wallets-data-table.tsx +1 -0
- package/src/components/ui/finance/wallets/wallets-page.tsx +5 -2
- package/src/components/ui/input-otp.tsx +1 -1
- package/src/components/ui/legacy/calendar/all-day-event-bar.tsx +28 -39
- package/src/components/ui/legacy/calendar/calendar-cell.tsx +2 -0
- package/src/components/ui/legacy/calendar/calendar-content.tsx +10 -6
- package/src/components/ui/legacy/calendar/calendar-header.tsx +23 -3
- package/src/components/ui/legacy/calendar/calendar-loading-skeleton.tsx +135 -0
- package/src/components/ui/legacy/calendar/calendar-matrix.tsx +175 -237
- package/src/components/ui/legacy/calendar/event-card.test.tsx +177 -0
- package/src/components/ui/legacy/calendar/event-card.tsx +220 -131
- package/src/components/ui/legacy/calendar/event-modal.tsx +17 -17
- package/src/components/ui/legacy/calendar/event-provider-display.tsx +69 -0
- package/src/components/ui/legacy/calendar/smart-calendar.test.tsx +86 -4
- package/src/components/ui/legacy/calendar/smart-calendar.tsx +32 -2
- package/src/components/ui/legacy/meet/create-plan-dialog.tsx +19 -10
- package/src/components/ui/navigation-menu.tsx +1 -1
- package/src/components/ui/pagination.tsx +1 -1
- package/src/components/ui/radio-group.tsx +1 -1
- package/src/components/ui/select.tsx +5 -1
- package/src/components/ui/sheet.tsx +1 -1
- package/src/components/ui/sidebar.tsx +1 -1
- package/src/components/ui/storefront/cart-popover.tsx +61 -0
- package/src/components/ui/storefront/cart-summary-parts.tsx +290 -0
- package/src/components/ui/storefront/cart-summary.tsx +93 -154
- package/src/components/ui/storefront/checkout-overlay.tsx +4 -5
- package/src/components/ui/storefront/listing-card.tsx +1 -1
- package/src/components/ui/storefront/merch-sections.tsx +70 -0
- package/src/components/ui/storefront/product-detail.tsx +1 -1
- package/src/components/ui/storefront/storefront-surface.test.tsx +106 -11
- package/src/components/ui/storefront/storefront-surface.tsx +101 -166
- package/src/components/ui/storefront/types.ts +4 -0
- package/src/components/ui/storefront/utils.ts +6 -0
- package/src/components/ui/text-editor/__tests__/extensions.test.ts +123 -0
- package/src/components/ui/text-editor/background-color-extension.ts +62 -0
- package/src/components/ui/text-editor/color-controls.tsx +284 -0
- package/src/components/ui/text-editor/editor.tsx +69 -14
- package/src/components/ui/text-editor/extensions.ts +8 -2
- package/src/components/ui/text-editor/highlight-extension.ts +22 -0
- package/src/components/ui/text-editor/tool-bar.tsx +9 -16
- package/src/components/ui/toast.tsx +1 -1
- package/src/components/ui/tu-do/boards/__tests__/board-share-dialog.test.tsx +270 -0
- package/src/components/ui/tu-do/boards/board-public-link-section.tsx +231 -0
- package/src/components/ui/tu-do/boards/board-share-dialog.tsx +222 -109
- package/src/components/ui/tu-do/boards/boardId/board-column.tsx +112 -43
- package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-mutations-clear-delete.ts +2 -0
- package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-mutations-move.ts +5 -0
- package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-mutations-updates.ts +3 -0
- package/src/components/ui/tu-do/boards/boardId/kanban/data/kanban-deadline-query.ts +50 -2
- package/src/components/ui/tu-do/boards/boardId/kanban/dnd/__tests__/column-reorder.test.ts +17 -0
- package/src/components/ui/tu-do/boards/boardId/kanban/dnd/column-reorder.ts +4 -1
- package/src/components/ui/tu-do/boards/boardId/kanban/dnd/task-drag-cache.ts +38 -9
- package/src/components/ui/tu-do/boards/boardId/kanban/dnd/task-drag-order.ts +2 -8
- package/src/components/ui/tu-do/boards/boardId/kanban/dnd/task-sort-key.ts +47 -0
- package/src/components/ui/tu-do/boards/boardId/kanban/dnd/use-kanban-dnd.ts +81 -30
- package/src/components/ui/tu-do/boards/boardId/kanban/planner/__tests__/kanban-planner-island.test.tsx +380 -0
- package/src/components/ui/tu-do/boards/boardId/kanban/planner/kanban-planner-dialog.tsx +204 -0
- package/src/components/ui/tu-do/boards/boardId/kanban/planner/planner-digest-panel.tsx +61 -0
- package/src/components/ui/tu-do/boards/boardId/kanban/planner/planner-item-strip.tsx +54 -0
- package/src/components/ui/tu-do/boards/boardId/kanban/planner/planner-plan-toolbar.tsx +251 -0
- package/src/components/ui/tu-do/boards/boardId/kanban/planner/planner-scope-badge.tsx +27 -0
- package/src/components/ui/tu-do/boards/boardId/kanban/planner/planner-section.tsx +58 -0
- package/src/components/ui/tu-do/boards/boardId/kanban/planner/planner-share-dialog.tsx +238 -0
- package/src/components/ui/tu-do/boards/boardId/kanban/planner/planner-target-controls.tsx +143 -0
- package/src/components/ui/tu-do/boards/boardId/kanban/planner/planner-utils.ts +65 -0
- package/src/components/ui/tu-do/boards/boardId/kanban/planner/use-kanban-planner-state.ts +234 -0
- package/src/components/ui/tu-do/boards/boardId/kanban/rendering/kanban-columns.test.tsx +397 -2
- package/src/components/ui/tu-do/boards/boardId/kanban/rendering/kanban-columns.tsx +103 -13
- package/src/components/ui/tu-do/boards/boardId/kanban/rendering/kanban-deadline-panels.tsx +443 -19
- package/src/components/ui/tu-do/boards/boardId/kanban/rendering/kanban-skeleton.tsx +94 -32
- package/src/components/ui/tu-do/boards/boardId/kanban.tsx +213 -106
- package/src/components/ui/tu-do/boards/boardId/task-board-server-page.test.tsx +26 -4
- package/src/components/ui/tu-do/boards/boardId/task-board-server-page.tsx +5 -2
- package/src/components/ui/tu-do/boards/boardId/task-card/measured-task-card.tsx +3 -0
- package/src/components/ui/tu-do/boards/boardId/task-card/task-card-comparator.ts +3 -0
- package/src/components/ui/tu-do/boards/boardId/task-card/task-card.tsx +191 -28
- package/src/components/ui/tu-do/boards/boardId/task-filter.test.tsx +152 -0
- package/src/components/ui/tu-do/boards/boardId/task-filter.tsx +555 -545
- package/src/components/ui/tu-do/boards/boardId/task-list.tsx +7 -0
- package/src/components/ui/tu-do/boards/share-section.tsx +100 -0
- package/src/components/ui/tu-do/drafts/draft-convert-dialog.tsx +10 -12
- package/src/components/ui/tu-do/drafts/drafts-page.tsx +33 -16
- package/src/components/ui/tu-do/initiatives/task-initiatives-client.tsx +56 -88
- package/src/components/ui/tu-do/my-tasks/my-tasks-content.tsx +26 -2
- package/src/components/ui/tu-do/my-tasks/use-my-tasks-state.ts +55 -8
- package/src/components/ui/tu-do/notes/note-edit-dialog.tsx +1 -4
- package/src/components/ui/tu-do/shared/__tests__/board-client.test.tsx +25 -0
- package/src/components/ui/tu-do/shared/__tests__/board-header.test.tsx +341 -38
- package/src/components/ui/tu-do/shared/__tests__/board-switcher.test.tsx +253 -0
- package/src/components/ui/tu-do/shared/__tests__/board-views.test.tsx +203 -2
- package/src/components/ui/tu-do/shared/__tests__/task-board-loading-state.test.tsx +17 -0
- package/src/components/ui/tu-do/shared/__tests__/task-legacy-route-recovery.test.tsx +16 -0
- package/src/components/ui/tu-do/shared/board-client.tsx +2 -7
- package/src/components/ui/tu-do/shared/board-config-storage.ts +7 -1
- package/src/components/ui/tu-do/shared/board-header.tsx +464 -975
- package/src/components/ui/tu-do/shared/board-layout-settings.tsx +165 -136
- package/src/components/ui/tu-do/shared/board-switcher.tsx +209 -217
- package/src/components/ui/tu-do/shared/board-views.tsx +587 -75
- package/src/components/ui/tu-do/shared/list-view.tsx +227 -1
- package/src/components/ui/tu-do/shared/recycle-bin-panel.tsx +142 -94
- package/src/components/ui/tu-do/shared/special-task-list-pins.ts +51 -0
- package/src/components/ui/tu-do/shared/task-board-loading-state.tsx +28 -0
- package/src/components/ui/tu-do/shared/task-edit-dialog/field-diff-viewer.tsx +3 -2
- package/src/components/ui/tu-do/shared/task-edit-dialog/selective-revert-panel.test.tsx +91 -0
- package/src/components/ui/tu-do/shared/task-edit-dialog/selective-revert-panel.tsx +123 -78
- package/src/components/ui/tu-do/shared/task-edit-dialog/task-activity-section.tsx +7 -1
- package/src/components/ui/tu-do/shared/task-edit-dialog/task-snapshot-dialog.tsx +8 -3
- package/src/components/ui/tu-do/shared/task-edit-dialog.tsx +2 -1
- package/src/components/ui/tu-do/shared/task-legacy-route-recovery.tsx +2 -9
- package/src/declarations.d.ts +1 -0
- package/src/hooks/__tests__/use-calendar-readonly.test.tsx +322 -2
- package/src/hooks/__tests__/use-calendar-sync.test.tsx +446 -0
- package/src/hooks/use-calendar-sync.tsx +247 -243
- package/src/hooks/use-calendar.tsx +323 -138
- package/src/hooks/use-task-actions.ts +24 -0
- package/src/hooks/use-user-workspace-config.ts +75 -0
- package/src/hooks/use-workspace-currency.ts +8 -3
- package/src/hooks/useBoardRealtimeEventHandler.ts +11 -0
|
@@ -1,216 +1,155 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import { ArrowRight
|
|
3
|
+
import { ArrowRight } from '@tuturuuu/icons';
|
|
4
4
|
import type { InventoryStorefront } from '@tuturuuu/internal-api/inventory';
|
|
5
5
|
import { cn } from '@tuturuuu/utils/format';
|
|
6
6
|
import type { FormEvent } from 'react';
|
|
7
7
|
import { Badge } from '../badge';
|
|
8
|
-
import { Button } from '../button';
|
|
9
8
|
import { AccentButton } from './accent-button';
|
|
10
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
CartActions,
|
|
11
|
+
CartContents,
|
|
12
|
+
CheckoutContactFields,
|
|
13
|
+
CheckoutSection,
|
|
14
|
+
} from './cart-summary-parts';
|
|
11
15
|
import type {
|
|
12
16
|
StorefrontBuyerDefaults,
|
|
13
17
|
StorefrontCartEntry,
|
|
14
18
|
StorefrontSurfaceLabels,
|
|
15
19
|
} from './types';
|
|
16
|
-
import {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
getStorefrontVariantLabel,
|
|
20
|
-
storefrontCartLineKey,
|
|
21
|
-
storefrontSurfaceClasses,
|
|
22
|
-
} from './utils';
|
|
20
|
+
import { formatStorefrontPrice, storefrontSurfaceClasses } from './utils';
|
|
21
|
+
|
|
22
|
+
type StorefrontCartSummaryVariant = 'checkout' | 'panel' | 'popover';
|
|
23
23
|
|
|
24
24
|
export function StorefrontCartSummary({
|
|
25
25
|
buyerDefaults,
|
|
26
26
|
cartEntries,
|
|
27
27
|
checkoutHref,
|
|
28
|
+
className,
|
|
28
29
|
currency,
|
|
29
30
|
isCheckout,
|
|
30
31
|
isPreview,
|
|
31
32
|
isSubmitting,
|
|
32
33
|
labels,
|
|
34
|
+
onCheckoutOpen,
|
|
33
35
|
onCheckoutSubmit,
|
|
34
36
|
onInstantCheckout,
|
|
35
37
|
radius,
|
|
36
38
|
storefront,
|
|
37
39
|
total,
|
|
40
|
+
variant,
|
|
38
41
|
}: {
|
|
39
42
|
buyerDefaults?: StorefrontBuyerDefaults;
|
|
40
43
|
cartEntries: StorefrontCartEntry[];
|
|
41
44
|
checkoutHref?: string;
|
|
45
|
+
className?: string;
|
|
42
46
|
currency: string;
|
|
43
|
-
isCheckout
|
|
47
|
+
isCheckout?: boolean;
|
|
44
48
|
isPreview: boolean;
|
|
45
49
|
isSubmitting: boolean;
|
|
46
50
|
labels: StorefrontSurfaceLabels;
|
|
51
|
+
onCheckoutOpen?: () => void;
|
|
47
52
|
onCheckoutSubmit?: (formData: FormData) => void;
|
|
48
53
|
onInstantCheckout?: () => void;
|
|
49
54
|
radius: string;
|
|
50
55
|
storefront: InventoryStorefront;
|
|
51
56
|
total: number;
|
|
57
|
+
variant?: StorefrontCartSummaryVariant;
|
|
52
58
|
}) {
|
|
59
|
+
const presentation = variant ?? (isCheckout ? 'checkout' : 'panel');
|
|
53
60
|
const hasCart = cartEntries.length > 0;
|
|
54
61
|
const isCheckoutDisabled = storefront.checkoutMode === 'disabled';
|
|
55
62
|
const submitDisabled =
|
|
56
63
|
!hasCart || isSubmitting || isCheckoutDisabled || !onCheckoutSubmit;
|
|
57
|
-
const canOpenCheckout = hasCart && Boolean(checkoutHref);
|
|
58
|
-
const buyerEmail = buyerDefaults?.email?.trim() || undefined;
|
|
59
|
-
const buyerName = buyerDefaults?.name?.trim() || undefined;
|
|
60
|
-
const inputClassName =
|
|
61
|
-
'h-11 rounded-md border border-input bg-background px-3 text-sm outline-none transition focus-visible:ring-2 focus-visible:ring-ring/40';
|
|
62
|
-
const labelClassName = 'grid gap-1.5 text-sm';
|
|
64
|
+
const canOpenCheckout = hasCart && Boolean(checkoutHref || onCheckoutOpen);
|
|
63
65
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
radius
|
|
71
|
-
)}
|
|
72
|
-
>
|
|
73
|
-
<div className="flex items-center justify-between gap-3">
|
|
74
|
-
<p className="font-semibold">{labels.cart}</p>
|
|
75
|
-
<Badge className="border-border bg-background" variant="outline">
|
|
76
|
-
{cartEntries.length}
|
|
77
|
-
</Badge>
|
|
78
|
-
</div>
|
|
79
|
-
<p className="mt-2 text-muted-foreground text-sm leading-6">
|
|
80
|
-
{labels.reservedCopy}
|
|
81
|
-
</p>
|
|
82
|
-
<div className="mt-4 -mr-1 grid max-h-72 gap-2.5 overflow-y-auto pr-1">
|
|
83
|
-
{cartEntries.map(({ line, listing, variant }) => {
|
|
84
|
-
const unitPrice = getStorefrontLinePrice(listing, variant);
|
|
85
|
-
const variantLabel = variant
|
|
86
|
-
? getStorefrontVariantLabel(variant)
|
|
87
|
-
: null;
|
|
88
|
-
return (
|
|
89
|
-
<div
|
|
90
|
-
className="flex items-center gap-3 text-sm"
|
|
91
|
-
key={storefrontCartLineKey(line.listingId, line.variantId)}
|
|
92
|
-
>
|
|
93
|
-
<StorefrontImagePanel
|
|
94
|
-
className={cn('size-10 shrink-0 rounded-md', radius)}
|
|
95
|
-
imageUrl={variant?.imageUrl ?? listing.imageUrl}
|
|
96
|
-
label={listing.title}
|
|
97
|
-
/>
|
|
98
|
-
<div className="min-w-0 flex-1">
|
|
99
|
-
<p className="truncate font-medium">{listing.title}</p>
|
|
100
|
-
{variantLabel ? (
|
|
101
|
-
<p className="truncate text-muted-foreground text-xs">
|
|
102
|
-
{variantLabel}
|
|
103
|
-
</p>
|
|
104
|
-
) : null}
|
|
105
|
-
<p className="truncate text-muted-foreground text-xs tabular-nums">
|
|
106
|
-
{line.quantity} × {formatStorefrontPrice(unitPrice, currency)}
|
|
107
|
-
</p>
|
|
108
|
-
</div>
|
|
109
|
-
<span className="shrink-0 whitespace-nowrap font-medium tabular-nums">
|
|
110
|
-
{formatStorefrontPrice(unitPrice * line.quantity, currency)}
|
|
111
|
-
</span>
|
|
112
|
-
</div>
|
|
113
|
-
);
|
|
114
|
-
})}
|
|
115
|
-
</div>
|
|
116
|
-
<div className="mt-4 flex items-center justify-between gap-2 border-border border-t pt-4">
|
|
117
|
-
<span className="text-muted-foreground text-sm">{labels.total}</span>
|
|
118
|
-
<span className="shrink-0 whitespace-nowrap font-semibold tabular-nums">
|
|
119
|
-
{formatStorefrontPrice(total, currency)}
|
|
120
|
-
</span>
|
|
121
|
-
</div>
|
|
122
|
-
{hasCart && !isCheckoutDisabled ? (
|
|
123
|
-
<p className="mt-3 flex items-center gap-2 rounded-md border border-border border-dashed bg-muted/30 px-3 py-2 text-muted-foreground text-xs leading-5">
|
|
124
|
-
<Tag className="h-3.5 w-3.5 shrink-0" />
|
|
125
|
-
{labels.couponNote}
|
|
126
|
-
</p>
|
|
127
|
-
) : null}
|
|
128
|
-
{!hasCart ? (
|
|
129
|
-
<p className="mt-4 flex items-center gap-2 rounded-md border border-border bg-muted/40 px-3 py-2 text-muted-foreground text-sm">
|
|
130
|
-
<TriangleAlert className="h-4 w-4" />
|
|
131
|
-
{labels.emptyCart}
|
|
132
|
-
</p>
|
|
133
|
-
) : null}
|
|
134
|
-
{isCheckout ? (
|
|
66
|
+
if (presentation === 'checkout') {
|
|
67
|
+
return (
|
|
68
|
+
<section
|
|
69
|
+
aria-label={labels.checkout}
|
|
70
|
+
className={cn('grid gap-5', className)}
|
|
71
|
+
>
|
|
135
72
|
<form
|
|
136
|
-
className="
|
|
73
|
+
className="grid gap-5"
|
|
137
74
|
onSubmit={(event: FormEvent<HTMLFormElement>) => {
|
|
138
75
|
event.preventDefault();
|
|
139
76
|
onCheckoutSubmit?.(new FormData(event.currentTarget));
|
|
140
77
|
}}
|
|
141
78
|
>
|
|
142
|
-
<
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
<span className="font-medium text-xs">{labels.form.email}</span>
|
|
155
|
-
<input
|
|
156
|
-
autoComplete="email"
|
|
157
|
-
className={inputClassName}
|
|
158
|
-
defaultValue={buyerEmail}
|
|
159
|
-
name="customerEmail"
|
|
160
|
-
placeholder={labels.form.email}
|
|
161
|
-
required
|
|
162
|
-
type="email"
|
|
79
|
+
<CheckoutSection
|
|
80
|
+
defaultOpen={cartEntries.length > 1}
|
|
81
|
+
meta={formatStorefrontPrice(total, currency)}
|
|
82
|
+
title={labels.orderSummary}
|
|
83
|
+
>
|
|
84
|
+
<CartContents
|
|
85
|
+
cartEntries={cartEntries}
|
|
86
|
+
currency={currency}
|
|
87
|
+
hasCart={hasCart}
|
|
88
|
+
isCheckoutDisabled={isCheckoutDisabled}
|
|
89
|
+
labels={labels}
|
|
90
|
+
total={total}
|
|
163
91
|
/>
|
|
164
|
-
</
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
<
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
name="customerPhone"
|
|
171
|
-
placeholder={labels.form.phone}
|
|
172
|
-
type="tel"
|
|
92
|
+
</CheckoutSection>
|
|
93
|
+
|
|
94
|
+
<CheckoutSection defaultOpen title={labels.contactDetails}>
|
|
95
|
+
<CheckoutContactFields
|
|
96
|
+
buyerDefaults={buyerDefaults}
|
|
97
|
+
labels={labels}
|
|
173
98
|
/>
|
|
174
|
-
</
|
|
175
|
-
|
|
176
|
-
className="min-h-24 rounded-md border border-input bg-background px-3 py-2.5 text-sm outline-none transition focus-visible:ring-2 focus-visible:ring-ring/40"
|
|
177
|
-
name="note"
|
|
178
|
-
placeholder={labels.form.note}
|
|
179
|
-
/>
|
|
99
|
+
</CheckoutSection>
|
|
100
|
+
|
|
180
101
|
<AccentButton disabled={submitDisabled} radius={radius}>
|
|
181
102
|
{isSubmitting ? labels.reserving : labels.reserve}
|
|
182
103
|
<ArrowRight className="size-4 shrink-0" />
|
|
183
104
|
</AccentButton>
|
|
184
105
|
</form>
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
{isSubmitting ? labels.reserving : labels.instantCheckout}
|
|
199
|
-
</AccentButton>
|
|
200
|
-
) : null}
|
|
201
|
-
<Button asChild className={cn('w-full', radius)} variant="outline">
|
|
202
|
-
<a href={checkoutHref}>
|
|
203
|
-
{labels.checkout}
|
|
204
|
-
<ArrowRight className="size-4 shrink-0" />
|
|
205
|
-
</a>
|
|
206
|
-
</Button>
|
|
207
|
-
</div>
|
|
208
|
-
) : (
|
|
209
|
-
<Button className={cn('mt-4 w-full', radius)} disabled type="button">
|
|
210
|
-
{labels.checkout}
|
|
211
|
-
<ArrowRight className="size-4 shrink-0" />
|
|
212
|
-
</Button>
|
|
106
|
+
</section>
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return (
|
|
111
|
+
<section
|
|
112
|
+
aria-label={labels.cart}
|
|
113
|
+
className={cn(
|
|
114
|
+
'h-fit',
|
|
115
|
+
presentation === 'panel'
|
|
116
|
+
? cn('p-4', storefrontSurfaceClasses[storefront.surfaceStyle], radius)
|
|
117
|
+
: 'grid gap-4',
|
|
118
|
+
className
|
|
213
119
|
)}
|
|
214
|
-
|
|
120
|
+
>
|
|
121
|
+
<div>
|
|
122
|
+
<div className="flex items-center justify-between gap-3">
|
|
123
|
+
<p className="font-semibold">{labels.cart}</p>
|
|
124
|
+
<Badge className="border-border bg-background" variant="outline">
|
|
125
|
+
{cartEntries.length}
|
|
126
|
+
</Badge>
|
|
127
|
+
</div>
|
|
128
|
+
<p className="mt-2 text-muted-foreground text-sm leading-6">
|
|
129
|
+
{labels.reservedCopy}
|
|
130
|
+
</p>
|
|
131
|
+
</div>
|
|
132
|
+
|
|
133
|
+
<CartContents
|
|
134
|
+
cartEntries={cartEntries}
|
|
135
|
+
currency={currency}
|
|
136
|
+
hasCart={hasCart}
|
|
137
|
+
isCheckoutDisabled={isCheckoutDisabled}
|
|
138
|
+
labels={labels}
|
|
139
|
+
total={total}
|
|
140
|
+
/>
|
|
141
|
+
|
|
142
|
+
<CartActions
|
|
143
|
+
canOpenCheckout={canOpenCheckout}
|
|
144
|
+
checkoutHref={checkoutHref}
|
|
145
|
+
isCheckoutDisabled={isCheckoutDisabled}
|
|
146
|
+
isPreview={isPreview}
|
|
147
|
+
isSubmitting={isSubmitting}
|
|
148
|
+
labels={labels}
|
|
149
|
+
onCheckoutOpen={onCheckoutOpen}
|
|
150
|
+
onInstantCheckout={onInstantCheckout}
|
|
151
|
+
radius={radius}
|
|
152
|
+
/>
|
|
153
|
+
</section>
|
|
215
154
|
);
|
|
216
155
|
}
|
|
@@ -3,10 +3,9 @@
|
|
|
3
3
|
import { Loader2 } from '@tuturuuu/icons';
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
|
-
* Full-screen blocking overlay shown while
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* sees progress instead of a frozen button.
|
|
6
|
+
* Full-screen blocking overlay shown while the checkout session or embedded
|
|
7
|
+
* payment intent is opening, so the buyer sees progress instead of a frozen
|
|
8
|
+
* button.
|
|
10
9
|
*/
|
|
11
10
|
export function StorefrontCheckoutOverlay({ label }: { label: string }) {
|
|
12
11
|
return (
|
|
@@ -17,7 +16,7 @@ export function StorefrontCheckoutOverlay({ label }: { label: string }) {
|
|
|
17
16
|
role="status"
|
|
18
17
|
>
|
|
19
18
|
<div className="flex flex-col items-center gap-4 px-6 text-center">
|
|
20
|
-
<span className="grid size-14 place-items-center rounded-full bg-[var(--storefront-accent,var(--
|
|
19
|
+
<span className="grid size-14 place-items-center rounded-full bg-[var(--storefront-accent-soft,var(--muted))] text-[var(--storefront-accent-text,var(--primary))]">
|
|
21
20
|
<Loader2 className="size-7 animate-spin" />
|
|
22
21
|
</span>
|
|
23
22
|
<p className="font-medium text-sm">{label}</p>
|
|
@@ -92,7 +92,7 @@ export function StorefrontListingCard({
|
|
|
92
92
|
<div className="min-w-0">
|
|
93
93
|
<div className="flex flex-wrap items-center gap-2">
|
|
94
94
|
<button
|
|
95
|
-
className="min-w-0 truncate text-left font-semibold transition hover:text-[var(--storefront-accent,var(--primary))] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/40 disabled:cursor-default disabled:hover:text-foreground"
|
|
95
|
+
className="min-w-0 truncate text-left font-semibold transition hover:text-[var(--storefront-accent-text,var(--primary))] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/40 disabled:cursor-default disabled:hover:text-foreground"
|
|
96
96
|
disabled={!openDetail}
|
|
97
97
|
onClick={openDetail}
|
|
98
98
|
type="button"
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import type { InventoryStorefrontSection } from '@tuturuuu/internal-api/inventory';
|
|
2
|
+
import { cn } from '@tuturuuu/utils/format';
|
|
3
|
+
import { StorefrontImagePanel } from './image-panel';
|
|
4
|
+
import { getSafeStorefrontHttpUrl } from './utils';
|
|
5
|
+
|
|
6
|
+
export function StorefrontMerchSections({
|
|
7
|
+
radius,
|
|
8
|
+
sections,
|
|
9
|
+
}: {
|
|
10
|
+
radius: string;
|
|
11
|
+
sections: InventoryStorefrontSection[];
|
|
12
|
+
}) {
|
|
13
|
+
const visibleSections = sections
|
|
14
|
+
.filter((section) => section.status === 'published')
|
|
15
|
+
.filter((section) => section.sectionType !== 'cover')
|
|
16
|
+
.filter((section) => {
|
|
17
|
+
const sectionHref = getSafeStorefrontHttpUrl(section.href);
|
|
18
|
+
return Boolean(
|
|
19
|
+
section.title?.trim() ||
|
|
20
|
+
section.description?.trim() ||
|
|
21
|
+
section.imageUrl?.trim() ||
|
|
22
|
+
sectionHref
|
|
23
|
+
);
|
|
24
|
+
})
|
|
25
|
+
.sort((a, b) => a.sortOrder - b.sortOrder);
|
|
26
|
+
|
|
27
|
+
if (visibleSections.length === 0) return null;
|
|
28
|
+
|
|
29
|
+
return (
|
|
30
|
+
<div className="mt-4 grid gap-3">
|
|
31
|
+
{visibleSections.map((section) => {
|
|
32
|
+
const sectionHref = getSafeStorefrontHttpUrl(section.href);
|
|
33
|
+
|
|
34
|
+
return (
|
|
35
|
+
<section
|
|
36
|
+
className={cn(
|
|
37
|
+
'grid overflow-hidden border border-border bg-card md:grid-cols-[minmax(0,1fr)_280px]',
|
|
38
|
+
radius
|
|
39
|
+
)}
|
|
40
|
+
key={section.id}
|
|
41
|
+
>
|
|
42
|
+
<div className="flex min-w-0 flex-col justify-center gap-2 p-4">
|
|
43
|
+
{section.title ? (
|
|
44
|
+
<h2 className="font-semibold text-lg">{section.title}</h2>
|
|
45
|
+
) : null}
|
|
46
|
+
{section.description ? (
|
|
47
|
+
<p className="text-muted-foreground text-sm leading-6">
|
|
48
|
+
{section.description}
|
|
49
|
+
</p>
|
|
50
|
+
) : null}
|
|
51
|
+
{sectionHref ? (
|
|
52
|
+
<a
|
|
53
|
+
className="mt-1 w-fit font-medium text-sm underline-offset-4 hover:underline"
|
|
54
|
+
href={sectionHref}
|
|
55
|
+
>
|
|
56
|
+
{sectionHref.replace(/^https?:\/\//u, '')}
|
|
57
|
+
</a>
|
|
58
|
+
) : null}
|
|
59
|
+
</div>
|
|
60
|
+
<StorefrontImagePanel
|
|
61
|
+
className="min-h-36 md:min-h-full"
|
|
62
|
+
imageUrl={section.imageUrl}
|
|
63
|
+
label={section.title ?? 'Storefront section'}
|
|
64
|
+
/>
|
|
65
|
+
</section>
|
|
66
|
+
);
|
|
67
|
+
})}
|
|
68
|
+
</div>
|
|
69
|
+
);
|
|
70
|
+
}
|
|
@@ -182,7 +182,7 @@ export function StorefrontProductDetail({
|
|
|
182
182
|
'inline-flex h-10 items-center justify-center border px-3 font-medium text-sm transition focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/40',
|
|
183
183
|
radius,
|
|
184
184
|
isActive
|
|
185
|
-
? 'border-[var(--storefront-accent,var(--
|
|
185
|
+
? 'border-[var(--storefront-accent-border,var(--border))] bg-[var(--storefront-accent-soft,var(--muted))] text-[var(--storefront-accent-text,var(--primary))]'
|
|
186
186
|
: 'border-border bg-card hover:bg-muted/45'
|
|
187
187
|
)}
|
|
188
188
|
key={value.id}
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import { render, screen } from '@testing-library/react';
|
|
1
|
+
import { fireEvent, render, screen } from '@testing-library/react';
|
|
2
2
|
import type {
|
|
3
3
|
InventoryStorefront,
|
|
4
4
|
InventoryStorefrontListing,
|
|
5
5
|
} from '@tuturuuu/internal-api/inventory';
|
|
6
6
|
import { describe, expect, it } from 'vitest';
|
|
7
7
|
import { StorefrontSurface } from './storefront-surface';
|
|
8
|
-
import { sanitizeStorefrontAccentColor } from './utils';
|
|
8
|
+
import { formatStorefrontPrice, sanitizeStorefrontAccentColor } from './utils';
|
|
9
9
|
|
|
10
10
|
const storefront: InventoryStorefront = {
|
|
11
11
|
accentColor: '#abc',
|
|
@@ -56,6 +56,15 @@ const listing: InventoryStorefrontListing = {
|
|
|
56
56
|
wsId: storefront.wsId,
|
|
57
57
|
};
|
|
58
58
|
|
|
59
|
+
const secondListing: InventoryStorefrontListing = {
|
|
60
|
+
...listing,
|
|
61
|
+
id: 'listing-2',
|
|
62
|
+
price: 2500,
|
|
63
|
+
productId: 'product-2',
|
|
64
|
+
title: 'Team Workshop With Long Name',
|
|
65
|
+
unitId: 'unit-2',
|
|
66
|
+
};
|
|
67
|
+
|
|
59
68
|
describe('StorefrontSurface', () => {
|
|
60
69
|
it('sanitizes hex accent colors only', () => {
|
|
61
70
|
expect(sanitizeStorefrontAccentColor('#abc')).toBe('#aabbcc');
|
|
@@ -80,10 +89,11 @@ describe('StorefrontSurface', () => {
|
|
|
80
89
|
expect(screen.getAllByText('Preview Store')).toHaveLength(2);
|
|
81
90
|
expect(screen.getByText('No buyer listings')).toBeInTheDocument();
|
|
82
91
|
expect(screen.getByText('Create a listing next.')).toBeInTheDocument();
|
|
92
|
+
fireEvent.click(screen.getByRole('button', { name: 'Cart: 0' }));
|
|
83
93
|
expect(screen.getByText('Preview checkout disabled')).toBeDisabled();
|
|
84
94
|
});
|
|
85
95
|
|
|
86
|
-
it('links storefront chrome back to the store and
|
|
96
|
+
it('links storefront chrome back to the store and opens cart from the header popover', () => {
|
|
87
97
|
render(
|
|
88
98
|
<StorefrontSurface
|
|
89
99
|
cartHref="/preview-store/cart"
|
|
@@ -100,10 +110,68 @@ describe('StorefrontSurface', () => {
|
|
|
100
110
|
'/preview-store'
|
|
101
111
|
);
|
|
102
112
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
expect(
|
|
113
|
+
expect(screen.queryByText('$2.00')).not.toBeInTheDocument();
|
|
114
|
+
|
|
115
|
+
const cartButton = screen.getByRole('button', { name: 'Cart: 2' });
|
|
116
|
+
expect(cartButton).toHaveClass('h-11', 'min-w-14', 'shrink-0');
|
|
117
|
+
expect(cartButton.querySelector('svg')).toHaveClass('size-5', 'shrink-0');
|
|
118
|
+
|
|
119
|
+
fireEvent.click(cartButton);
|
|
120
|
+
|
|
121
|
+
expect(screen.getByRole('region', { name: 'Cart' })).toBeInTheDocument();
|
|
122
|
+
expect(screen.getAllByText('$2.00')).toHaveLength(2);
|
|
123
|
+
expect(screen.getAllByText('1M')).toHaveLength(1);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it('keeps the compatibility cart page as a full cart review', () => {
|
|
127
|
+
render(
|
|
128
|
+
<StorefrontSurface
|
|
129
|
+
cartHref="/preview-store/cart"
|
|
130
|
+
cartLines={[{ listingId: listing.id, quantity: 2 }]}
|
|
131
|
+
checkoutHref="/preview-store/checkout"
|
|
132
|
+
listings={[listing]}
|
|
133
|
+
mode="cart"
|
|
134
|
+
onCheckoutOpen={() => undefined}
|
|
135
|
+
storefront={storefront}
|
|
136
|
+
storefrontHref="/preview-store"
|
|
137
|
+
/>
|
|
138
|
+
);
|
|
139
|
+
|
|
140
|
+
expect(screen.getByRole('region', { name: 'Cart' })).toBeInTheDocument();
|
|
141
|
+
expect(screen.getByText('1:1 Mentoring')).toBeInTheDocument();
|
|
142
|
+
expect(screen.getAllByText('$2.00')).toHaveLength(2);
|
|
143
|
+
expect(screen.getByRole('button', { name: /Checkout/ })).toBeEnabled();
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it('keeps long item names and large totals inside the cart row bounds', () => {
|
|
147
|
+
const largeListing: InventoryStorefrontListing = {
|
|
148
|
+
...listing,
|
|
149
|
+
price: 123_456_789,
|
|
150
|
+
title:
|
|
151
|
+
'A very long product title with specifications, measurements, and buyer-facing detail',
|
|
152
|
+
};
|
|
153
|
+
const expectedTotal = formatStorefrontPrice(largeListing.price * 5, 'USD');
|
|
154
|
+
|
|
155
|
+
render(
|
|
156
|
+
<StorefrontSurface
|
|
157
|
+
cartLines={[{ listingId: largeListing.id, quantity: 5 }]}
|
|
158
|
+
listings={[largeListing]}
|
|
159
|
+
mode="cart"
|
|
160
|
+
storefront={storefront}
|
|
161
|
+
/>
|
|
162
|
+
);
|
|
163
|
+
|
|
164
|
+
const amount = screen.getByTitle(expectedTotal);
|
|
165
|
+
expect(amount).toHaveClass(
|
|
166
|
+
'max-w-[9rem]',
|
|
167
|
+
'overflow-hidden',
|
|
168
|
+
'text-ellipsis',
|
|
169
|
+
'text-right'
|
|
170
|
+
);
|
|
171
|
+
expect(screen.getByText(largeListing.title)).toHaveClass(
|
|
172
|
+
'line-clamp-2',
|
|
173
|
+
'break-words'
|
|
174
|
+
);
|
|
107
175
|
});
|
|
108
176
|
|
|
109
177
|
it('prefills checkout buyer details while keeping editable form fields', () => {
|
|
@@ -113,14 +181,23 @@ describe('StorefrontSurface', () => {
|
|
|
113
181
|
email: 'buyer@example.com',
|
|
114
182
|
name: 'Sokora Buyer',
|
|
115
183
|
}}
|
|
116
|
-
cartLines={[
|
|
117
|
-
|
|
184
|
+
cartLines={[
|
|
185
|
+
{ listingId: listing.id, quantity: 1 },
|
|
186
|
+
{ listingId: secondListing.id, quantity: 1 },
|
|
187
|
+
]}
|
|
188
|
+
listings={[listing, secondListing]}
|
|
118
189
|
mode="checkout"
|
|
119
190
|
onCheckoutSubmit={() => undefined}
|
|
120
191
|
storefront={storefront}
|
|
121
192
|
/>
|
|
122
193
|
);
|
|
123
194
|
|
|
195
|
+
expect(
|
|
196
|
+
screen.getByRole('button', { name: /Order summary/ })
|
|
197
|
+
).toBeInTheDocument();
|
|
198
|
+
expect(
|
|
199
|
+
screen.getByRole('button', { name: 'Contact details' })
|
|
200
|
+
).toBeInTheDocument();
|
|
124
201
|
expect(screen.getByLabelText('Name')).toHaveValue('Sokora Buyer');
|
|
125
202
|
expect(screen.getByLabelText('Email')).toHaveValue('buyer@example.com');
|
|
126
203
|
expect(screen.getByLabelText('Name')).toBeEnabled();
|
|
@@ -155,6 +232,7 @@ describe('StorefrontSurface', () => {
|
|
|
155
232
|
);
|
|
156
233
|
|
|
157
234
|
expect(screen.queryByText('Checkout disabled')).not.toBeInTheDocument();
|
|
235
|
+
fireEvent.click(screen.getByRole('button', { name: 'Cart: 0' }));
|
|
158
236
|
expect(screen.getByText('Checkout unavailable')).toBeDisabled();
|
|
159
237
|
});
|
|
160
238
|
|
|
@@ -166,6 +244,22 @@ describe('StorefrontSurface', () => {
|
|
|
166
244
|
storefront={{
|
|
167
245
|
...storefront,
|
|
168
246
|
sections: [
|
|
247
|
+
{
|
|
248
|
+
createdAt: null,
|
|
249
|
+
description: null,
|
|
250
|
+
href: null,
|
|
251
|
+
id: 'section-empty',
|
|
252
|
+
imageUrl: null,
|
|
253
|
+
items: [],
|
|
254
|
+
metadata: {},
|
|
255
|
+
sectionType: 'promo',
|
|
256
|
+
sortOrder: 0,
|
|
257
|
+
status: 'published',
|
|
258
|
+
storefrontId: storefront.id,
|
|
259
|
+
title: null,
|
|
260
|
+
updatedAt: null,
|
|
261
|
+
wsId: storefront.wsId,
|
|
262
|
+
},
|
|
169
263
|
{
|
|
170
264
|
createdAt: null,
|
|
171
265
|
description: null,
|
|
@@ -175,7 +269,7 @@ describe('StorefrontSurface', () => {
|
|
|
175
269
|
items: [],
|
|
176
270
|
metadata: {},
|
|
177
271
|
sectionType: 'promo',
|
|
178
|
-
sortOrder:
|
|
272
|
+
sortOrder: 1,
|
|
179
273
|
status: 'published',
|
|
180
274
|
storefrontId: storefront.id,
|
|
181
275
|
title: 'Unsafe section',
|
|
@@ -191,7 +285,7 @@ describe('StorefrontSurface', () => {
|
|
|
191
285
|
items: [],
|
|
192
286
|
metadata: {},
|
|
193
287
|
sectionType: 'promo',
|
|
194
|
-
sortOrder:
|
|
288
|
+
sortOrder: 2,
|
|
195
289
|
status: 'published',
|
|
196
290
|
storefrontId: storefront.id,
|
|
197
291
|
title: 'Safe section',
|
|
@@ -204,6 +298,7 @@ describe('StorefrontSurface', () => {
|
|
|
204
298
|
);
|
|
205
299
|
|
|
206
300
|
expect(screen.getByText('Safe section')).toBeInTheDocument();
|
|
301
|
+
expect(screen.queryByText('Storefront section')).not.toBeInTheDocument();
|
|
207
302
|
expect(
|
|
208
303
|
screen.getByRole('link', { name: 'example.com/promo' })
|
|
209
304
|
).toHaveAttribute('href', 'https://example.com/promo');
|