@tuturuuu/ui 0.4.1 → 0.6.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 +43 -0
- package/package.json +41 -34
- package/src/components/ui/currency-input.tsx +65 -23
- package/src/components/ui/custom/__tests__/sidebar-context.test.tsx +64 -0
- package/src/components/ui/custom/__tests__/sidebar-remote-behavior-bridge.test.tsx +109 -0
- package/src/components/ui/custom/combobox.test.tsx +141 -0
- package/src/components/ui/custom/combobox.tsx +105 -36
- package/src/components/ui/custom/settings/task-settings.tsx +126 -0
- package/src/components/ui/custom/settings/task-sound-settings.test.tsx +146 -0
- package/src/components/ui/custom/sidebar-context.tsx +68 -6
- package/src/components/ui/custom/sidebar-remote-behavior-bridge.tsx +21 -2
- package/src/components/ui/finance/finance-layout.tsx +2 -4
- package/src/components/ui/finance/shared/balance-mode-toggle.tsx +35 -0
- package/src/components/ui/finance/shared/finance-layout-controls.tsx +43 -0
- package/src/components/ui/finance/shared/quick-actions.tsx +14 -6
- package/src/components/ui/finance/shared/use-finance-balance-mode.ts +72 -0
- package/src/components/ui/finance/shared/wallet-balance-mode.test.ts +66 -0
- package/src/components/ui/finance/shared/wallet-balance-mode.ts +42 -0
- package/src/components/ui/finance/transactions/form-types.ts +23 -0
- package/src/components/ui/finance/transactions/form.tsx +81 -22
- package/src/components/ui/finance/transactions/infinite-transactions-list.tsx +29 -18
- package/src/components/ui/finance/transactions/transaction-card.tsx +75 -43
- package/src/components/ui/finance/transactions/transfer-merge.test.ts +90 -0
- package/src/components/ui/finance/transactions/transfer-merge.ts +52 -0
- package/src/components/ui/finance/transactions/wallet-filter.tsx +21 -2
- package/src/components/ui/finance/wallets/checkpoints/wallet-checkpoint-adjustment-dialog.tsx +219 -0
- package/src/components/ui/finance/wallets/checkpoints/wallet-checkpoint-amount.tsx +32 -0
- package/src/components/ui/finance/wallets/checkpoints/wallet-checkpoint-delete-dialog.tsx +50 -0
- package/src/components/ui/finance/wallets/checkpoints/wallet-checkpoint-dialog.tsx +138 -0
- package/src/components/ui/finance/wallets/checkpoints/wallet-checkpoint-history-dialog.tsx +617 -0
- package/src/components/ui/finance/wallets/checkpoints/wallet-checkpoint-panel.tsx +197 -0
- package/src/components/ui/finance/wallets/checkpoints/wallet-checkpoint-sections.tsx +201 -0
- package/src/components/ui/finance/wallets/checkpoints/wallet-checkpoints.test.tsx +541 -0
- package/src/components/ui/finance/wallets/checkpoints/wallet-total-check-dialog.tsx +362 -0
- package/src/components/ui/finance/wallets/columns-rendering.test.tsx +125 -0
- package/src/components/ui/finance/wallets/columns.test.ts +56 -0
- package/src/components/ui/finance/wallets/columns.tsx +196 -43
- package/src/components/ui/finance/wallets/form.test.tsx +79 -14
- package/src/components/ui/finance/wallets/form.tsx +41 -197
- package/src/components/ui/finance/wallets/query-invalidation.ts +3 -0
- package/src/components/ui/finance/wallets/wallet-basics-fields.tsx +141 -0
- package/src/components/ui/finance/wallets/wallet-credit-fields.tsx +136 -0
- package/src/components/ui/finance/wallets/walletId/credit-wallet-summary.tsx +143 -68
- package/src/components/ui/finance/wallets/walletId/wallet-details-actions.test.tsx +105 -0
- package/src/components/ui/finance/wallets/walletId/wallet-details-actions.tsx +120 -16
- package/src/components/ui/finance/wallets/walletId/wallet-details-amount.test.tsx +64 -0
- package/src/components/ui/finance/wallets/walletId/wallet-details-amount.tsx +226 -6
- package/src/components/ui/finance/wallets/walletId/wallet-details-page.test.tsx +71 -5
- package/src/components/ui/finance/wallets/walletId/wallet-details-page.tsx +52 -35
- package/src/components/ui/finance/wallets/wallets-data-table.test.tsx +171 -0
- package/src/components/ui/finance/wallets/wallets-data-table.tsx +132 -29
- package/src/components/ui/finance/wallets/wallets-page.test.tsx +117 -36
- package/src/components/ui/finance/wallets/wallets-page.tsx +40 -64
- package/src/components/ui/storefront/accent-button.tsx +33 -0
- package/src/components/ui/storefront/cart-summary.tsx +140 -0
- package/src/components/ui/storefront/empty-listings.tsx +32 -0
- package/src/components/ui/storefront/hero-panel.tsx +70 -0
- package/src/components/ui/storefront/image-panel.tsx +40 -0
- package/src/components/ui/storefront/index.ts +12 -0
- package/src/components/ui/storefront/listing-card.tsx +129 -0
- package/src/components/ui/storefront/storefront-surface.test.tsx +85 -0
- package/src/components/ui/storefront/storefront-surface.tsx +235 -0
- package/src/components/ui/storefront/types.ts +99 -0
- package/src/components/ui/storefront/utils.ts +90 -0
- package/src/components/ui/tu-do/boards/boardId/kanban/bulk/__tests__/bulk-mutations-move.test.tsx +14 -0
- package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-operations.ts +29 -0
- package/src/components/ui/tu-do/boards/boardId/task-card/task-card-open-options.test.ts +134 -0
- package/src/components/ui/tu-do/boards/boardId/task-card/task-card-open-options.ts +127 -0
- package/src/components/ui/tu-do/boards/boardId/task-card/task-card.tsx +17 -42
- package/src/components/ui/tu-do/boards/boardId/timeline-board-open-task.test.tsx +164 -0
- package/src/components/ui/tu-do/boards/boardId/timeline-board.tsx +25 -16
- package/src/components/ui/tu-do/hooks/useTaskDialog.ts +15 -1
- package/src/components/ui/tu-do/my-tasks/__tests__/use-task-context-actions.test.ts +11 -0
- package/src/components/ui/tu-do/my-tasks/use-my-tasks-state.ts +2 -0
- package/src/components/ui/tu-do/my-tasks/use-task-context-actions.ts +124 -7
- package/src/components/ui/tu-do/providers/__tests__/task-dialog-provider.test.tsx +217 -5
- package/src/components/ui/tu-do/providers/task-dialog-provider.tsx +180 -35
- package/src/components/ui/tu-do/shared/__tests__/task-dialog-manager.test.tsx +222 -26
- package/src/components/ui/tu-do/shared/board-client.tsx +1 -3
- package/src/components/ui/tu-do/shared/list-view-context-menu.test.tsx +55 -2
- package/src/components/ui/tu-do/shared/list-view.tsx +23 -16
- package/src/components/ui/tu-do/shared/task-dialog-manager.tsx +93 -76
- package/src/components/ui/tu-do/shared/task-dialog-presentation.ts +11 -0
- package/src/components/ui/tu-do/shared/task-edit-dialog/components/compact-task-create-popover.test.tsx +268 -0
- package/src/components/ui/tu-do/shared/task-edit-dialog/components/compact-task-create-popover.tsx +243 -0
- package/src/components/ui/tu-do/shared/task-edit-dialog/components/quick-settings-popover.tsx +26 -0
- package/src/components/ui/tu-do/shared/task-edit-dialog/components/smart-task-suggestions-panel.test.tsx +129 -0
- package/src/components/ui/tu-do/shared/task-edit-dialog/components/smart-task-suggestions-panel.tsx +358 -0
- package/src/components/ui/tu-do/shared/task-edit-dialog/components/task-description-editor.tsx +1 -1
- package/src/components/ui/tu-do/shared/task-edit-dialog/components/task-dialog-header.tsx +6 -2
- package/src/components/ui/tu-do/shared/task-edit-dialog/components/task-list-selector.tsx +36 -20
- package/src/components/ui/tu-do/shared/task-edit-dialog/components/task-name-input.test.tsx +41 -1
- package/src/components/ui/tu-do/shared/task-edit-dialog/components/task-name-input.tsx +157 -102
- package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/use-task-form-reset.ts +18 -2
- package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/use-task-realtime-sync.ts +1 -2
- package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/use-task-save.test.ts +84 -1
- package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/use-task-save.ts +5 -1
- package/src/components/ui/tu-do/shared/task-edit-dialog/task-dialog-actions.tsx +5 -3
- package/src/components/ui/tu-do/shared/task-edit-dialog/task-properties-section.tsx +300 -172
- package/src/components/ui/tu-do/shared/task-edit-dialog.tsx +959 -340
- package/src/components/ui/tu-do/shared/task-sound-effects.test.ts +189 -0
- package/src/components/ui/tu-do/shared/task-sound-effects.tsx +468 -0
- package/src/hooks/__tests__/use-task-actions.test.tsx +61 -0
- package/src/hooks/use-task-actions.ts +45 -0
- package/src/hooks/useBoardRealtime.ts +54 -1
- package/src/hooks/useBoardRealtimeEventHandler.ts +169 -4
- package/src/hooks/useTaskUserRealtime.ts +338 -0
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { render, screen } from '@testing-library/react';
|
|
2
|
+
import type { InventoryStorefront } from '@tuturuuu/internal-api/inventory';
|
|
3
|
+
import { describe, expect, it } from 'vitest';
|
|
4
|
+
import { StorefrontSurface } from './storefront-surface';
|
|
5
|
+
import { sanitizeStorefrontAccentColor } from './utils';
|
|
6
|
+
|
|
7
|
+
const storefront: InventoryStorefront = {
|
|
8
|
+
accentColor: '#abc',
|
|
9
|
+
cornerStyle: 'rounded',
|
|
10
|
+
createdAt: '2026-06-12T00:00:00.000Z',
|
|
11
|
+
currency: 'USD',
|
|
12
|
+
checkoutMode: 'polar',
|
|
13
|
+
description: 'Buyer-facing copy',
|
|
14
|
+
heroImageUrl: null,
|
|
15
|
+
id: 'storefront-1',
|
|
16
|
+
layoutStyle: 'grid',
|
|
17
|
+
listingsCount: 0,
|
|
18
|
+
name: 'Preview Store',
|
|
19
|
+
showInventoryBadges: true,
|
|
20
|
+
slug: 'preview-store',
|
|
21
|
+
status: 'published',
|
|
22
|
+
surfaceStyle: 'soft',
|
|
23
|
+
themePreset: 'catalog',
|
|
24
|
+
updatedAt: '2026-06-12T00:00:00.000Z',
|
|
25
|
+
visibility: 'public',
|
|
26
|
+
wsId: 'ws-1',
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
describe('StorefrontSurface', () => {
|
|
30
|
+
it('sanitizes hex accent colors only', () => {
|
|
31
|
+
expect(sanitizeStorefrontAccentColor('#abc')).toBe('#aabbcc');
|
|
32
|
+
expect(sanitizeStorefrontAccentColor('#123abc')).toBe('#123abc');
|
|
33
|
+
expect(sanitizeStorefrontAccentColor('red')).toBeNull();
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('renders a preview empty state without enabling checkout', () => {
|
|
37
|
+
render(
|
|
38
|
+
<StorefrontSurface
|
|
39
|
+
labels={{
|
|
40
|
+
checkoutDisabled: 'Preview checkout disabled',
|
|
41
|
+
emptyListingsDescription: 'Create a listing next.',
|
|
42
|
+
emptyListingsTitle: 'No buyer listings',
|
|
43
|
+
}}
|
|
44
|
+
listings={[]}
|
|
45
|
+
mode="preview"
|
|
46
|
+
storefront={storefront}
|
|
47
|
+
/>
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
expect(screen.getAllByText('Preview Store')).toHaveLength(2);
|
|
51
|
+
expect(screen.getByText('No buyer listings')).toBeInTheDocument();
|
|
52
|
+
expect(screen.getByText('Create a listing next.')).toBeInTheDocument();
|
|
53
|
+
expect(screen.getByText('Preview checkout disabled')).toBeDisabled();
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('shows simulated checkout mode badges', () => {
|
|
57
|
+
render(
|
|
58
|
+
<StorefrontSurface
|
|
59
|
+
labels={{ simulatedBadge: 'Simulated checkout' }}
|
|
60
|
+
listings={[]}
|
|
61
|
+
mode="store"
|
|
62
|
+
storefront={{ ...storefront, checkoutMode: 'simulated' }}
|
|
63
|
+
/>
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
expect(screen.getByText('Simulated checkout')).toBeInTheDocument();
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('shows disabled checkout mode badges and blocks checkout', () => {
|
|
70
|
+
render(
|
|
71
|
+
<StorefrontSurface
|
|
72
|
+
labels={{
|
|
73
|
+
checkoutDisabled: 'Checkout unavailable',
|
|
74
|
+
checkoutDisabledBadge: 'Checkout disabled',
|
|
75
|
+
}}
|
|
76
|
+
listings={[]}
|
|
77
|
+
mode="store"
|
|
78
|
+
storefront={{ ...storefront, checkoutMode: 'disabled' }}
|
|
79
|
+
/>
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
expect(screen.getByText('Checkout disabled')).toBeInTheDocument();
|
|
83
|
+
expect(screen.getByText('Checkout unavailable')).toBeDisabled();
|
|
84
|
+
});
|
|
85
|
+
});
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { ShoppingCart } from '@tuturuuu/icons';
|
|
4
|
+
import type {
|
|
5
|
+
InventoryStorefront,
|
|
6
|
+
InventoryStorefrontListing,
|
|
7
|
+
} from '@tuturuuu/internal-api/inventory';
|
|
8
|
+
import { cn } from '@tuturuuu/utils/format';
|
|
9
|
+
import type { ReactNode } from 'react';
|
|
10
|
+
import { Badge } from '../badge';
|
|
11
|
+
import { StorefrontCartSummary } from './cart-summary';
|
|
12
|
+
import { StorefrontEmptyListings } from './empty-listings';
|
|
13
|
+
import { StorefrontHeroPanel } from './hero-panel';
|
|
14
|
+
import { StorefrontListingCard } from './listing-card';
|
|
15
|
+
import type {
|
|
16
|
+
StorefrontCartLine,
|
|
17
|
+
StorefrontSurfaceLabels,
|
|
18
|
+
StorefrontSurfaceMode,
|
|
19
|
+
} from './types';
|
|
20
|
+
import { mergeStorefrontSurfaceLabels } from './types';
|
|
21
|
+
import {
|
|
22
|
+
getAccentStyle,
|
|
23
|
+
getStorefrontListingLimit,
|
|
24
|
+
sanitizeStorefrontAccentColor,
|
|
25
|
+
storefrontRadiusClasses,
|
|
26
|
+
storefrontSurfaceClasses,
|
|
27
|
+
} from './utils';
|
|
28
|
+
|
|
29
|
+
export function StorefrontSurface({
|
|
30
|
+
cartLines = [],
|
|
31
|
+
checkoutHref,
|
|
32
|
+
className,
|
|
33
|
+
emptyAction,
|
|
34
|
+
headerActions,
|
|
35
|
+
isDemo = false,
|
|
36
|
+
isSubmitting = false,
|
|
37
|
+
labels: labelOverrides,
|
|
38
|
+
listings,
|
|
39
|
+
mode,
|
|
40
|
+
notice,
|
|
41
|
+
onCheckoutSubmit,
|
|
42
|
+
onDecrement,
|
|
43
|
+
onIncrement,
|
|
44
|
+
selectedListingId,
|
|
45
|
+
storefront,
|
|
46
|
+
}: {
|
|
47
|
+
cartLines?: StorefrontCartLine[];
|
|
48
|
+
checkoutHref?: string;
|
|
49
|
+
className?: string;
|
|
50
|
+
emptyAction?: ReactNode;
|
|
51
|
+
headerActions?: ReactNode;
|
|
52
|
+
isDemo?: boolean;
|
|
53
|
+
isSubmitting?: boolean;
|
|
54
|
+
labels?: Partial<StorefrontSurfaceLabels>;
|
|
55
|
+
listings: InventoryStorefrontListing[];
|
|
56
|
+
mode: StorefrontSurfaceMode;
|
|
57
|
+
notice?: ReactNode;
|
|
58
|
+
onCheckoutSubmit?: (formData: FormData) => void;
|
|
59
|
+
onDecrement?: (listingId: string) => void;
|
|
60
|
+
onIncrement?: (listingId: string, maxQuantity: number) => void;
|
|
61
|
+
selectedListingId?: string;
|
|
62
|
+
storefront: InventoryStorefront;
|
|
63
|
+
}) {
|
|
64
|
+
const labels = mergeStorefrontSurfaceLabels(labelOverrides);
|
|
65
|
+
const accentColor = sanitizeStorefrontAccentColor(storefront.accentColor);
|
|
66
|
+
const radius = storefrontRadiusClasses[storefront.cornerStyle];
|
|
67
|
+
const cartEntries = cartLines.flatMap((line) => {
|
|
68
|
+
const listing = listings.find((item) => item.id === line.listingId);
|
|
69
|
+
return listing ? [{ line, listing }] : [];
|
|
70
|
+
});
|
|
71
|
+
const checkoutEntries = cartEntries.filter(({ line, listing }) => {
|
|
72
|
+
const quantity = Math.min(
|
|
73
|
+
line.quantity,
|
|
74
|
+
getStorefrontListingLimit(listing)
|
|
75
|
+
);
|
|
76
|
+
return quantity > 0;
|
|
77
|
+
});
|
|
78
|
+
const total = checkoutEntries.reduce((sum, { line, listing }) => {
|
|
79
|
+
const quantity = Math.min(
|
|
80
|
+
line.quantity,
|
|
81
|
+
getStorefrontListingLimit(listing)
|
|
82
|
+
);
|
|
83
|
+
return sum + listing.price * quantity;
|
|
84
|
+
}, 0);
|
|
85
|
+
const cartQuantity = cartLines.reduce((sum, line) => sum + line.quantity, 0);
|
|
86
|
+
const visibleListings =
|
|
87
|
+
mode === 'product' && selectedListingId
|
|
88
|
+
? listings.filter((listing) => listing.id === selectedListingId)
|
|
89
|
+
: listings;
|
|
90
|
+
const isCheckout = mode === 'checkout';
|
|
91
|
+
const isPreview = mode === 'preview';
|
|
92
|
+
const showCartListings = mode === 'cart' || isCheckout;
|
|
93
|
+
const listingRows = showCartListings
|
|
94
|
+
? cartEntries.map(({ listing }) => listing)
|
|
95
|
+
: visibleListings;
|
|
96
|
+
const currency = storefront.currency ?? 'USD';
|
|
97
|
+
|
|
98
|
+
return (
|
|
99
|
+
<main
|
|
100
|
+
className={cn('min-h-dvh bg-background text-foreground', className)}
|
|
101
|
+
style={getAccentStyle(accentColor)}
|
|
102
|
+
>
|
|
103
|
+
{notice ? (
|
|
104
|
+
<div className="border-border border-b bg-muted/35 px-4 py-2 text-center text-muted-foreground text-sm">
|
|
105
|
+
{notice}
|
|
106
|
+
</div>
|
|
107
|
+
) : null}
|
|
108
|
+
|
|
109
|
+
<header className="border-border border-b bg-background/90 backdrop-blur">
|
|
110
|
+
<div className="mx-auto flex max-w-7xl flex-wrap items-center justify-between gap-3 px-4 py-3">
|
|
111
|
+
<div className="min-w-0">
|
|
112
|
+
<div className="flex flex-wrap items-center gap-2 text-muted-foreground text-xs">
|
|
113
|
+
<span>
|
|
114
|
+
{storefront.visibility === 'private'
|
|
115
|
+
? labels.privateStore
|
|
116
|
+
: labels.publicStore}
|
|
117
|
+
</span>
|
|
118
|
+
{isDemo ? (
|
|
119
|
+
<Badge variant="secondary">{labels.demoBadge}</Badge>
|
|
120
|
+
) : null}
|
|
121
|
+
{isPreview ? (
|
|
122
|
+
<Badge
|
|
123
|
+
className="border-border bg-background"
|
|
124
|
+
variant="outline"
|
|
125
|
+
>
|
|
126
|
+
{labels.previewBadge}
|
|
127
|
+
</Badge>
|
|
128
|
+
) : null}
|
|
129
|
+
{storefront.checkoutMode === 'simulated' ? (
|
|
130
|
+
<Badge
|
|
131
|
+
className="border-border bg-background"
|
|
132
|
+
variant="outline"
|
|
133
|
+
>
|
|
134
|
+
{labels.simulatedBadge}
|
|
135
|
+
</Badge>
|
|
136
|
+
) : null}
|
|
137
|
+
{storefront.checkoutMode === 'disabled' ? (
|
|
138
|
+
<Badge
|
|
139
|
+
className="border-border bg-background"
|
|
140
|
+
variant="outline"
|
|
141
|
+
>
|
|
142
|
+
{labels.checkoutDisabledBadge}
|
|
143
|
+
</Badge>
|
|
144
|
+
) : null}
|
|
145
|
+
</div>
|
|
146
|
+
<h1 className="mt-0.5 truncate font-semibold text-xl">
|
|
147
|
+
{storefront.name}
|
|
148
|
+
</h1>
|
|
149
|
+
</div>
|
|
150
|
+
<div className="flex items-center gap-2">
|
|
151
|
+
{headerActions}
|
|
152
|
+
<span
|
|
153
|
+
className={cn(
|
|
154
|
+
'inline-flex h-9 min-w-12 items-center justify-center gap-2 border bg-card px-3 font-medium text-sm',
|
|
155
|
+
radius
|
|
156
|
+
)}
|
|
157
|
+
>
|
|
158
|
+
<ShoppingCart className="h-4 w-4" />
|
|
159
|
+
{cartQuantity}
|
|
160
|
+
</span>
|
|
161
|
+
</div>
|
|
162
|
+
</div>
|
|
163
|
+
</header>
|
|
164
|
+
|
|
165
|
+
<section className="mx-auto grid max-w-7xl gap-4 px-4 py-5 lg:grid-cols-[minmax(0,1fr)_340px]">
|
|
166
|
+
<div className="min-w-0">
|
|
167
|
+
<StorefrontHeroPanel
|
|
168
|
+
currency={currency}
|
|
169
|
+
labels={labels}
|
|
170
|
+
listingsCount={listings.length}
|
|
171
|
+
radius={radius}
|
|
172
|
+
storefront={storefront}
|
|
173
|
+
/>
|
|
174
|
+
|
|
175
|
+
<div
|
|
176
|
+
className={cn(
|
|
177
|
+
'mt-4',
|
|
178
|
+
storefront.layoutStyle === 'list'
|
|
179
|
+
? 'grid gap-3'
|
|
180
|
+
: 'grid gap-3 sm:grid-cols-2 xl:grid-cols-3',
|
|
181
|
+
storefront.layoutStyle === 'feature' &&
|
|
182
|
+
'[&>article:first-child]:sm:col-span-2'
|
|
183
|
+
)}
|
|
184
|
+
>
|
|
185
|
+
{listingRows.length === 0 ? (
|
|
186
|
+
<StorefrontEmptyListings
|
|
187
|
+
action={emptyAction}
|
|
188
|
+
labels={labels}
|
|
189
|
+
radius={radius}
|
|
190
|
+
/>
|
|
191
|
+
) : (
|
|
192
|
+
listingRows.map((listing) => {
|
|
193
|
+
const line = cartLines.find(
|
|
194
|
+
(item) => item.listingId === listing.id
|
|
195
|
+
);
|
|
196
|
+
|
|
197
|
+
return (
|
|
198
|
+
<StorefrontListingCard
|
|
199
|
+
currency={currency}
|
|
200
|
+
isList={storefront.layoutStyle === 'list'}
|
|
201
|
+
key={listing.id}
|
|
202
|
+
labels={labels}
|
|
203
|
+
listing={listing}
|
|
204
|
+
onDecrement={onDecrement}
|
|
205
|
+
onIncrement={onIncrement}
|
|
206
|
+
quantity={line?.quantity ?? 0}
|
|
207
|
+
radius={radius}
|
|
208
|
+
showInventoryBadges={storefront.showInventoryBadges}
|
|
209
|
+
surfaceClassName={
|
|
210
|
+
storefrontSurfaceClasses[storefront.surfaceStyle]
|
|
211
|
+
}
|
|
212
|
+
/>
|
|
213
|
+
);
|
|
214
|
+
})
|
|
215
|
+
)}
|
|
216
|
+
</div>
|
|
217
|
+
</div>
|
|
218
|
+
|
|
219
|
+
<StorefrontCartSummary
|
|
220
|
+
cartEntries={checkoutEntries}
|
|
221
|
+
checkoutHref={checkoutHref}
|
|
222
|
+
currency={currency}
|
|
223
|
+
isCheckout={isCheckout}
|
|
224
|
+
isPreview={isPreview}
|
|
225
|
+
isSubmitting={isSubmitting}
|
|
226
|
+
labels={labels}
|
|
227
|
+
onCheckoutSubmit={onCheckoutSubmit}
|
|
228
|
+
radius={radius}
|
|
229
|
+
storefront={storefront}
|
|
230
|
+
total={total}
|
|
231
|
+
/>
|
|
232
|
+
</section>
|
|
233
|
+
</main>
|
|
234
|
+
);
|
|
235
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import type { InventoryStorefrontListing } from '@tuturuuu/internal-api/inventory';
|
|
2
|
+
|
|
3
|
+
export type StorefrontCartLine = {
|
|
4
|
+
listingId: string;
|
|
5
|
+
quantity: number;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
export type StorefrontCartEntry = {
|
|
9
|
+
line: StorefrontCartLine;
|
|
10
|
+
listing: InventoryStorefrontListing;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export type StorefrontSurfaceMode =
|
|
14
|
+
| 'cart'
|
|
15
|
+
| 'checkout'
|
|
16
|
+
| 'preview'
|
|
17
|
+
| 'product'
|
|
18
|
+
| 'store';
|
|
19
|
+
|
|
20
|
+
export type StorefrontSurfaceLabels = {
|
|
21
|
+
add: string;
|
|
22
|
+
available: string;
|
|
23
|
+
browse: string;
|
|
24
|
+
bundle: string;
|
|
25
|
+
cart: string;
|
|
26
|
+
checkout: string;
|
|
27
|
+
checkoutDisabled: string;
|
|
28
|
+
checkoutDisabledBadge: string;
|
|
29
|
+
demoBadge: string;
|
|
30
|
+
emptyCart: string;
|
|
31
|
+
emptyListingsDescription: string;
|
|
32
|
+
emptyListingsTitle: string;
|
|
33
|
+
fallbackDescription: string;
|
|
34
|
+
form: {
|
|
35
|
+
email: string;
|
|
36
|
+
name: string;
|
|
37
|
+
note: string;
|
|
38
|
+
phone: string;
|
|
39
|
+
};
|
|
40
|
+
privateStore: string;
|
|
41
|
+
previewBadge: string;
|
|
42
|
+
product: string;
|
|
43
|
+
publicStore: string;
|
|
44
|
+
quantity: string;
|
|
45
|
+
reserve: string;
|
|
46
|
+
reserving: string;
|
|
47
|
+
reservedCopy: string;
|
|
48
|
+
simulatedBadge: string;
|
|
49
|
+
soldOut: string;
|
|
50
|
+
total: string;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
export const defaultStorefrontSurfaceLabels: StorefrontSurfaceLabels = {
|
|
54
|
+
add: 'Add',
|
|
55
|
+
available: 'available',
|
|
56
|
+
browse: 'Browse',
|
|
57
|
+
bundle: 'Bundle',
|
|
58
|
+
cart: 'Cart',
|
|
59
|
+
checkout: 'Checkout',
|
|
60
|
+
checkoutDisabled: 'Checkout is disabled in preview',
|
|
61
|
+
checkoutDisabledBadge: 'Checkout disabled',
|
|
62
|
+
demoBadge: 'Demo',
|
|
63
|
+
emptyCart: 'Add a listing to start checkout.',
|
|
64
|
+
emptyListingsDescription:
|
|
65
|
+
'Publish a listing to make this storefront ready for buyers.',
|
|
66
|
+
emptyListingsTitle: 'No listings yet',
|
|
67
|
+
fallbackDescription: 'This listing is available for checkout.',
|
|
68
|
+
form: {
|
|
69
|
+
email: 'Email',
|
|
70
|
+
name: 'Name',
|
|
71
|
+
note: 'Order note',
|
|
72
|
+
phone: 'Phone',
|
|
73
|
+
},
|
|
74
|
+
privateStore: 'Private',
|
|
75
|
+
previewBadge: 'Preview',
|
|
76
|
+
product: 'Product',
|
|
77
|
+
publicStore: 'Public',
|
|
78
|
+
quantity: 'Qty',
|
|
79
|
+
reserve: 'Reserve with Polar',
|
|
80
|
+
reserving: 'Reserving...',
|
|
81
|
+
reservedCopy:
|
|
82
|
+
'Review your cart, then continue with the available checkout mode.',
|
|
83
|
+
simulatedBadge: 'Simulated checkout',
|
|
84
|
+
soldOut: 'Sold out',
|
|
85
|
+
total: 'Total',
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
export function mergeStorefrontSurfaceLabels(
|
|
89
|
+
labels?: Partial<StorefrontSurfaceLabels>
|
|
90
|
+
) {
|
|
91
|
+
return {
|
|
92
|
+
...defaultStorefrontSurfaceLabels,
|
|
93
|
+
...labels,
|
|
94
|
+
form: {
|
|
95
|
+
...defaultStorefrontSurfaceLabels.form,
|
|
96
|
+
...labels?.form,
|
|
97
|
+
},
|
|
98
|
+
};
|
|
99
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
InventoryStorefront,
|
|
3
|
+
InventoryStorefrontListing,
|
|
4
|
+
} from '@tuturuuu/internal-api/inventory';
|
|
5
|
+
import type { CSSProperties } from 'react';
|
|
6
|
+
|
|
7
|
+
export const storefrontRadiusClasses: Record<
|
|
8
|
+
InventoryStorefront['cornerStyle'],
|
|
9
|
+
string
|
|
10
|
+
> = {
|
|
11
|
+
compact: 'rounded-md',
|
|
12
|
+
rounded: 'rounded-lg',
|
|
13
|
+
soft: 'rounded-2xl',
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export const storefrontSurfaceClasses: Record<
|
|
17
|
+
InventoryStorefront['surfaceStyle'],
|
|
18
|
+
string
|
|
19
|
+
> = {
|
|
20
|
+
glass:
|
|
21
|
+
'border border-border/70 bg-card/75 shadow-sm shadow-foreground/5 backdrop-blur',
|
|
22
|
+
soft: 'border border-border/70 bg-muted/35 shadow-sm shadow-foreground/5',
|
|
23
|
+
solid: 'border border-border bg-card shadow-sm shadow-foreground/5',
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export type StorefrontAccentStyle = CSSProperties & {
|
|
27
|
+
'--storefront-accent'?: string;
|
|
28
|
+
'--storefront-accent-foreground'?: string;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export function sanitizeStorefrontAccentColor(value?: string | null) {
|
|
32
|
+
const normalized = value?.trim();
|
|
33
|
+
if (!normalized) return null;
|
|
34
|
+
|
|
35
|
+
if (/^#[0-9a-f]{3}$/i.test(normalized)) {
|
|
36
|
+
const [, r, g, b] = normalized;
|
|
37
|
+
return `#${r}${r}${g}${g}${b}${b}`.toLowerCase();
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (/^#[0-9a-f]{6}$/i.test(normalized)) {
|
|
41
|
+
return normalized.toLowerCase();
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function getStorefrontListingLimit(listing: InventoryStorefrontListing) {
|
|
48
|
+
const available =
|
|
49
|
+
typeof listing.availableQuantity === 'number'
|
|
50
|
+
? listing.availableQuantity
|
|
51
|
+
: Number.POSITIVE_INFINITY;
|
|
52
|
+
|
|
53
|
+
return Math.max(0, Math.min(listing.maxPerOrder, available));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function formatStorefrontPrice(value: number, currency: string) {
|
|
57
|
+
return new Intl.NumberFormat(undefined, {
|
|
58
|
+
currency,
|
|
59
|
+
maximumFractionDigits: 0,
|
|
60
|
+
style: 'currency',
|
|
61
|
+
}).format(value);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function getListingInitials(title: string) {
|
|
65
|
+
return title
|
|
66
|
+
.split(/\s+/)
|
|
67
|
+
.filter(Boolean)
|
|
68
|
+
.slice(0, 2)
|
|
69
|
+
.map((part) => part[0]?.toUpperCase())
|
|
70
|
+
.join('');
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function getAccentStyle(
|
|
74
|
+
accentColor: string | null
|
|
75
|
+
): StorefrontAccentStyle {
|
|
76
|
+
if (!accentColor) return {};
|
|
77
|
+
|
|
78
|
+
return {
|
|
79
|
+
'--storefront-accent': accentColor,
|
|
80
|
+
'--storefront-accent-foreground': getAccentForeground(accentColor),
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function getAccentForeground(hexColor: string) {
|
|
85
|
+
const red = Number.parseInt(hexColor.slice(1, 3), 16);
|
|
86
|
+
const green = Number.parseInt(hexColor.slice(3, 5), 16);
|
|
87
|
+
const blue = Number.parseInt(hexColor.slice(5, 7), 16);
|
|
88
|
+
const luminance = (0.299 * red + 0.587 * green + 0.114 * blue) / 255;
|
|
89
|
+
return luminance > 0.64 ? '#111111' : '#ffffff';
|
|
90
|
+
}
|
package/src/components/ui/tu-do/boards/boardId/kanban/bulk/__tests__/bulk-mutations-move.test.tsx
CHANGED
|
@@ -10,6 +10,10 @@ import type { ReactElement, ReactNode } from 'react';
|
|
|
10
10
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
11
11
|
import { useBulkOperations } from '../bulk-operations';
|
|
12
12
|
|
|
13
|
+
const { mockDispatchTaskSoundCue } = vi.hoisted(() => ({
|
|
14
|
+
mockDispatchTaskSoundCue: vi.fn(),
|
|
15
|
+
}));
|
|
16
|
+
|
|
13
17
|
vi.mock('next-intl', () => ({
|
|
14
18
|
useTranslations: () => {
|
|
15
19
|
const translate = (key: string, values?: Record<string, unknown>) =>
|
|
@@ -49,6 +53,10 @@ vi.mock('@tuturuuu/ui/sonner', () => ({
|
|
|
49
53
|
},
|
|
50
54
|
}));
|
|
51
55
|
|
|
56
|
+
vi.mock('../../../../../shared/task-sound-effects', () => ({
|
|
57
|
+
dispatchTaskSoundCue: mockDispatchTaskSoundCue,
|
|
58
|
+
}));
|
|
59
|
+
|
|
52
60
|
describe('bulk move mutations', () => {
|
|
53
61
|
let queryClient: QueryClient;
|
|
54
62
|
let wrapper: ({ children }: { children: ReactNode }) => ReactElement;
|
|
@@ -172,5 +180,11 @@ describe('bulk move mutations', () => {
|
|
|
172
180
|
personal_list_id: 'list-2',
|
|
173
181
|
})
|
|
174
182
|
);
|
|
183
|
+
expect(mockDispatchTaskSoundCue).toHaveBeenCalledTimes(1);
|
|
184
|
+
expect(mockDispatchTaskSoundCue).toHaveBeenCalledWith({
|
|
185
|
+
count: 2,
|
|
186
|
+
cue: 'move',
|
|
187
|
+
intensity: 1.2,
|
|
188
|
+
});
|
|
175
189
|
});
|
|
176
190
|
});
|
|
@@ -2,6 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
import type { Task } from '@tuturuuu/types/primitives/Task';
|
|
4
4
|
import { useEffect } from 'react';
|
|
5
|
+
import {
|
|
6
|
+
dispatchTaskSoundCue,
|
|
7
|
+
type TaskSoundCue,
|
|
8
|
+
} from '../../../../shared/task-sound-effects';
|
|
5
9
|
import {
|
|
6
10
|
useBulkClearAssignees,
|
|
7
11
|
useBulkClearLabels,
|
|
@@ -31,6 +35,14 @@ import { useBulkOperationI18n } from './bulk-operation-i18n';
|
|
|
31
35
|
import type { BulkOperationsConfig } from './bulk-operation-types';
|
|
32
36
|
import { getBulkTaskWorkspaceGroups } from './bulk-operation-utils';
|
|
33
37
|
|
|
38
|
+
function dispatchBulkTaskSoundCue(cue: TaskSoundCue, count: number) {
|
|
39
|
+
dispatchTaskSoundCue({
|
|
40
|
+
cue,
|
|
41
|
+
count,
|
|
42
|
+
intensity: count > 1 ? 1.2 : 1,
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
|
|
34
46
|
export function useBulkOperations(config: BulkOperationsConfig) {
|
|
35
47
|
const i18n = useBulkOperationI18n();
|
|
36
48
|
|
|
@@ -213,11 +225,13 @@ export function useBulkOperations(config: BulkOperationsConfig) {
|
|
|
213
225
|
const taskIds = Array.from(selectedTasks);
|
|
214
226
|
if (!taskIds.length) return;
|
|
215
227
|
await priorityMutation.mutateAsync({ priority, taskIds });
|
|
228
|
+
dispatchBulkTaskSoundCue('update', taskIds.length);
|
|
216
229
|
},
|
|
217
230
|
bulkUpdateEstimation: async (points: number | null) => {
|
|
218
231
|
const taskIds = Array.from(selectedTasks);
|
|
219
232
|
if (!taskIds.length) return;
|
|
220
233
|
await estimationMutation.mutateAsync({ points, taskIds });
|
|
234
|
+
dispatchBulkTaskSoundCue('update', taskIds.length);
|
|
221
235
|
},
|
|
222
236
|
bulkUpdateDueDate: async (
|
|
223
237
|
preset: 'today' | 'tomorrow' | 'this_week' | 'next_week' | 'clear'
|
|
@@ -225,16 +239,19 @@ export function useBulkOperations(config: BulkOperationsConfig) {
|
|
|
225
239
|
const taskIds = Array.from(selectedTasks);
|
|
226
240
|
if (!taskIds.length) return;
|
|
227
241
|
await dueDateMutation.mutateAsync({ preset, taskIds });
|
|
242
|
+
dispatchBulkTaskSoundCue('update', taskIds.length);
|
|
228
243
|
},
|
|
229
244
|
bulkUpdateCustomDueDate: async (date: Date | null) => {
|
|
230
245
|
const taskIds = Array.from(selectedTasks);
|
|
231
246
|
if (!taskIds.length) return;
|
|
232
247
|
await customDueDateMutation.mutateAsync({ date, taskIds });
|
|
248
|
+
dispatchBulkTaskSoundCue('update', taskIds.length);
|
|
233
249
|
},
|
|
234
250
|
bulkMoveToList: async (listId: string, listName: string) => {
|
|
235
251
|
const taskIds = Array.from(selectedTasks);
|
|
236
252
|
if (!taskIds.length) return;
|
|
237
253
|
await moveToListMutation.mutateAsync({ listId, listName, taskIds });
|
|
254
|
+
dispatchBulkTaskSoundCue('move', taskIds.length);
|
|
238
255
|
},
|
|
239
256
|
bulkMoveToStatus: async (status: 'done' | 'closed') => {
|
|
240
257
|
const taskIds = Array.from(selectedTasks);
|
|
@@ -242,51 +259,61 @@ export function useBulkOperations(config: BulkOperationsConfig) {
|
|
|
242
259
|
const listId = getListIdByStatus(status);
|
|
243
260
|
if (!listId) return;
|
|
244
261
|
await statusMutation.mutateAsync({ status, taskIds });
|
|
262
|
+
dispatchBulkTaskSoundCue('complete', taskIds.length);
|
|
245
263
|
},
|
|
246
264
|
bulkAddLabel: async (labelId: string) => {
|
|
247
265
|
const taskIds = Array.from(selectedTasks);
|
|
248
266
|
if (!taskIds.length) return;
|
|
249
267
|
await addLabelMutation.mutateAsync({ labelId, taskIds });
|
|
268
|
+
dispatchBulkTaskSoundCue('update', taskIds.length);
|
|
250
269
|
},
|
|
251
270
|
bulkRemoveLabel: async (labelId: string) => {
|
|
252
271
|
const taskIds = Array.from(selectedTasks);
|
|
253
272
|
if (!taskIds.length) return;
|
|
254
273
|
await removeLabelMutation.mutateAsync({ labelId, taskIds });
|
|
274
|
+
dispatchBulkTaskSoundCue('update', taskIds.length);
|
|
255
275
|
},
|
|
256
276
|
bulkAddProject: async (projectId: string) => {
|
|
257
277
|
const taskIds = Array.from(selectedTasks);
|
|
258
278
|
if (!taskIds.length) return;
|
|
259
279
|
await addProjectMutation.mutateAsync({ projectId, taskIds });
|
|
280
|
+
dispatchBulkTaskSoundCue('update', taskIds.length);
|
|
260
281
|
},
|
|
261
282
|
bulkRemoveProject: async (projectId: string) => {
|
|
262
283
|
const taskIds = Array.from(selectedTasks);
|
|
263
284
|
if (!taskIds.length) return;
|
|
264
285
|
await removeProjectMutation.mutateAsync({ projectId, taskIds });
|
|
286
|
+
dispatchBulkTaskSoundCue('update', taskIds.length);
|
|
265
287
|
},
|
|
266
288
|
bulkAddAssignee: async (assigneeId: string) => {
|
|
267
289
|
const taskIds = Array.from(selectedTasks);
|
|
268
290
|
if (!taskIds.length) return;
|
|
269
291
|
await addAssigneeMutation.mutateAsync({ assigneeId, taskIds });
|
|
292
|
+
dispatchBulkTaskSoundCue('update', taskIds.length);
|
|
270
293
|
},
|
|
271
294
|
bulkRemoveAssignee: async (assigneeId: string) => {
|
|
272
295
|
const taskIds = Array.from(selectedTasks);
|
|
273
296
|
if (!taskIds.length) return;
|
|
274
297
|
await removeAssigneeMutation.mutateAsync({ assigneeId, taskIds });
|
|
298
|
+
dispatchBulkTaskSoundCue('update', taskIds.length);
|
|
275
299
|
},
|
|
276
300
|
bulkClearLabels: async () => {
|
|
277
301
|
const taskIds = Array.from(selectedTasks);
|
|
278
302
|
if (!taskIds.length) return;
|
|
279
303
|
await clearLabelsMutation.mutateAsync({ taskIds });
|
|
304
|
+
dispatchBulkTaskSoundCue('update', taskIds.length);
|
|
280
305
|
},
|
|
281
306
|
bulkClearProjects: async () => {
|
|
282
307
|
const taskIds = Array.from(selectedTasks);
|
|
283
308
|
if (!taskIds.length) return;
|
|
284
309
|
await clearProjectsMutation.mutateAsync({ taskIds });
|
|
310
|
+
dispatchBulkTaskSoundCue('update', taskIds.length);
|
|
285
311
|
},
|
|
286
312
|
bulkClearAssignees: async () => {
|
|
287
313
|
const taskIds = Array.from(selectedTasks);
|
|
288
314
|
if (!taskIds.length) return;
|
|
289
315
|
await clearAssigneesMutation.mutateAsync({ taskIds });
|
|
316
|
+
dispatchBulkTaskSoundCue('update', taskIds.length);
|
|
290
317
|
},
|
|
291
318
|
bulkDeleteTasks: async () => {
|
|
292
319
|
const taskIds = Array.from(selectedTasks);
|
|
@@ -298,6 +325,7 @@ export function useBulkOperations(config: BulkOperationsConfig) {
|
|
|
298
325
|
taskIds,
|
|
299
326
|
});
|
|
300
327
|
await deleteMutation.mutateAsync({ taskIds, workspaceGroups });
|
|
328
|
+
dispatchBulkTaskSoundCue('delete', taskIds.length);
|
|
301
329
|
},
|
|
302
330
|
bulkMoveToBoard: async (targetBoardId: string, targetListId: string) => {
|
|
303
331
|
const taskIds = Array.from(selectedTasks);
|
|
@@ -307,6 +335,7 @@ export function useBulkOperations(config: BulkOperationsConfig) {
|
|
|
307
335
|
targetListId,
|
|
308
336
|
taskIds,
|
|
309
337
|
});
|
|
338
|
+
dispatchBulkTaskSoundCue('move', taskIds.length);
|
|
310
339
|
},
|
|
311
340
|
getListIdByStatus,
|
|
312
341
|
};
|