@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
|
@@ -1,10 +1,8 @@
|
|
|
1
|
-
import {
|
|
2
|
-
type InternalApiClientOptions,
|
|
3
|
-
listWallets,
|
|
4
|
-
withForwardedInternalApiAuth,
|
|
5
|
-
} from '@tuturuuu/internal-api';
|
|
6
|
-
import type { Wallet } from '@tuturuuu/types/primitives/Wallet';
|
|
1
|
+
import { FinanceBalanceModeToggle } from '@tuturuuu/ui/finance/shared/balance-mode-toggle';
|
|
7
2
|
import { CreateDialogFeatureSummary } from '@tuturuuu/ui/finance/shared/create-dialog-feature-summary';
|
|
3
|
+
import { FinanceNumbersVisibilityToggle } from '@tuturuuu/ui/finance/shared/numbers-visibility-toggle';
|
|
4
|
+
import { WalletCheckpointHistoryDialog } from '@tuturuuu/ui/finance/wallets/checkpoints/wallet-checkpoint-history-dialog';
|
|
5
|
+
import { WalletTotalCheckDialog } from '@tuturuuu/ui/finance/wallets/checkpoints/wallet-total-check-dialog';
|
|
8
6
|
import { WalletForm } from '@tuturuuu/ui/finance/wallets/form';
|
|
9
7
|
import { WalletsDataTable } from '@tuturuuu/ui/finance/wallets/wallets-data-table';
|
|
10
8
|
import { Separator } from '@tuturuuu/ui/separator';
|
|
@@ -14,7 +12,6 @@ import {
|
|
|
14
12
|
getWorkspaceConfig,
|
|
15
13
|
type PermissionsResult,
|
|
16
14
|
} from '@tuturuuu/utils/workspace-helper';
|
|
17
|
-
import { headers } from 'next/headers';
|
|
18
15
|
import { notFound } from 'next/navigation';
|
|
19
16
|
import { getTranslations } from 'next-intl/server';
|
|
20
17
|
|
|
@@ -22,15 +19,10 @@ interface Props {
|
|
|
22
19
|
wsId: string;
|
|
23
20
|
searchParams: {
|
|
24
21
|
create?: string;
|
|
25
|
-
q
|
|
26
|
-
page: string;
|
|
27
|
-
pageSize: string;
|
|
22
|
+
q?: string;
|
|
28
23
|
};
|
|
29
|
-
page?: string;
|
|
30
|
-
pageSize?: string;
|
|
31
24
|
currency?: string;
|
|
32
25
|
financePrefix?: string;
|
|
33
|
-
internalApiOptions?: InternalApiClientOptions;
|
|
34
26
|
openCreateDialog?: boolean;
|
|
35
27
|
permissions?: PermissionsResult;
|
|
36
28
|
workspace?: {
|
|
@@ -41,11 +33,8 @@ interface Props {
|
|
|
41
33
|
export default async function WalletsPage({
|
|
42
34
|
wsId,
|
|
43
35
|
searchParams,
|
|
44
|
-
page,
|
|
45
|
-
pageSize,
|
|
46
36
|
currency,
|
|
47
37
|
financePrefix = '/finance',
|
|
48
|
-
internalApiOptions,
|
|
49
38
|
openCreateDialog = false,
|
|
50
39
|
permissions,
|
|
51
40
|
workspace,
|
|
@@ -65,19 +54,8 @@ export default async function WalletsPage({
|
|
|
65
54
|
const canCreateWallets = containsPermission('create_wallets');
|
|
66
55
|
const canUpdateWallets = containsPermission('update_wallets');
|
|
67
56
|
const canDeleteWallets = containsPermission('delete_wallets');
|
|
68
|
-
const
|
|
69
|
-
|
|
70
|
-
const { data: rawData, count } = await getData(
|
|
71
|
-
wsId,
|
|
72
|
-
searchParams,
|
|
73
|
-
resolvedInternalApiOptions
|
|
74
|
-
);
|
|
75
|
-
|
|
76
|
-
const data = rawData.map((d) => ({
|
|
77
|
-
...d,
|
|
78
|
-
href: `/${wsId}${financePrefix}/wallets/${d.id}`,
|
|
79
|
-
ws_id: wsId,
|
|
80
|
-
}));
|
|
57
|
+
const canCreateTransactions = containsPermission('create_transactions');
|
|
58
|
+
const isCreditCardCreate = searchParams.create === 'credit-card';
|
|
81
59
|
|
|
82
60
|
return (
|
|
83
61
|
<>
|
|
@@ -87,50 +65,48 @@ export default async function WalletsPage({
|
|
|
87
65
|
description={t('ws-wallets.description')}
|
|
88
66
|
createTitle={t('ws-wallets.create')}
|
|
89
67
|
createDescription={t('ws-wallets.create_description')}
|
|
90
|
-
defaultOpen={
|
|
91
|
-
|
|
68
|
+
defaultOpen={
|
|
69
|
+
openCreateDialog ||
|
|
70
|
+
searchParams.create === 'wallet' ||
|
|
71
|
+
isCreditCardCreate
|
|
72
|
+
}
|
|
73
|
+
form={
|
|
74
|
+
canCreateWallets ? (
|
|
75
|
+
<WalletForm
|
|
76
|
+
wsId={wsId}
|
|
77
|
+
defaultType={isCreditCardCreate ? 'CREDIT' : 'STANDARD'}
|
|
78
|
+
/>
|
|
79
|
+
) : undefined
|
|
80
|
+
}
|
|
92
81
|
/>
|
|
93
82
|
<Separator className="my-4" />
|
|
83
|
+
<div className="mb-4 flex flex-col gap-2 lg:flex-row lg:items-center lg:justify-between">
|
|
84
|
+
<div className="flex flex-wrap gap-2">
|
|
85
|
+
<FinanceBalanceModeToggle />
|
|
86
|
+
<FinanceNumbersVisibilityToggle />
|
|
87
|
+
</div>
|
|
88
|
+
<div className="flex flex-wrap gap-2 lg:justify-end">
|
|
89
|
+
<WalletCheckpointHistoryDialog
|
|
90
|
+
wsId={wsId}
|
|
91
|
+
financePrefix={financePrefix}
|
|
92
|
+
canCreateTransactions={canCreateTransactions}
|
|
93
|
+
/>
|
|
94
|
+
<WalletTotalCheckDialog
|
|
95
|
+
wsId={wsId}
|
|
96
|
+
currency={resolvedCurrency ?? 'USD'}
|
|
97
|
+
canUpdateWallets={canUpdateWallets}
|
|
98
|
+
/>
|
|
99
|
+
</div>
|
|
100
|
+
</div>
|
|
94
101
|
<WalletsDataTable
|
|
95
102
|
wsId={wsId}
|
|
96
|
-
data={data}
|
|
97
|
-
count={count}
|
|
98
103
|
canUpdateWallets={canUpdateWallets}
|
|
99
104
|
canDeleteWallets={canDeleteWallets}
|
|
100
105
|
currency={resolvedCurrency ?? 'USD'}
|
|
106
|
+
financePrefix={financePrefix}
|
|
101
107
|
isPersonalWorkspace={!!resolvedWorkspace?.personal}
|
|
102
|
-
|
|
103
|
-
pageSize={pageSize}
|
|
108
|
+
query={searchParams.q}
|
|
104
109
|
/>
|
|
105
110
|
</>
|
|
106
111
|
);
|
|
107
112
|
}
|
|
108
|
-
|
|
109
|
-
async function getData(
|
|
110
|
-
wsId: string,
|
|
111
|
-
{
|
|
112
|
-
q,
|
|
113
|
-
page = '1',
|
|
114
|
-
pageSize = '10',
|
|
115
|
-
}: { q?: string; page?: string; pageSize?: string },
|
|
116
|
-
internalApiOptions: Parameters<typeof listWallets>[1]
|
|
117
|
-
) {
|
|
118
|
-
const wallets = await listWallets(wsId, internalApiOptions);
|
|
119
|
-
const normalizedQuery = q?.trim().toLowerCase();
|
|
120
|
-
const filteredWallets = wallets
|
|
121
|
-
.filter((wallet) =>
|
|
122
|
-
normalizedQuery
|
|
123
|
-
? wallet.name?.toLowerCase().includes(normalizedQuery)
|
|
124
|
-
: true
|
|
125
|
-
)
|
|
126
|
-
.sort((a, b) => (a.name || '').localeCompare(b.name || ''));
|
|
127
|
-
|
|
128
|
-
const parsedPage = parseInt(page, 10);
|
|
129
|
-
const parsedPageSize = parseInt(pageSize, 10);
|
|
130
|
-
const start = (parsedPage - 1) * parsedPageSize;
|
|
131
|
-
|
|
132
|
-
return {
|
|
133
|
-
data: filteredWallets.slice(start, start + parsedPageSize) as Wallet[],
|
|
134
|
-
count: filteredWallets.length,
|
|
135
|
-
};
|
|
136
|
-
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { cn } from '@tuturuuu/utils/format';
|
|
4
|
+
import type { ReactNode } from 'react';
|
|
5
|
+
|
|
6
|
+
export function AccentButton({
|
|
7
|
+
children,
|
|
8
|
+
disabled,
|
|
9
|
+
onClick,
|
|
10
|
+
radius,
|
|
11
|
+
}: {
|
|
12
|
+
children: ReactNode;
|
|
13
|
+
disabled?: boolean;
|
|
14
|
+
onClick?: () => void;
|
|
15
|
+
radius: string;
|
|
16
|
+
}) {
|
|
17
|
+
return (
|
|
18
|
+
<button
|
|
19
|
+
className={cn(
|
|
20
|
+
'inline-flex h-9 items-center justify-center gap-2 px-3 font-medium text-sm transition disabled:pointer-events-none disabled:opacity-50',
|
|
21
|
+
'bg-primary text-primary-foreground hover:bg-primary/90',
|
|
22
|
+
'[--accent-bg:var(--storefront-accent,var(--primary))] [--accent-fg:var(--storefront-accent-foreground,var(--primary-foreground))]',
|
|
23
|
+
'bg-[var(--accent-bg)] text-[var(--accent-fg)] hover:opacity-90',
|
|
24
|
+
radius
|
|
25
|
+
)}
|
|
26
|
+
disabled={disabled}
|
|
27
|
+
onClick={onClick}
|
|
28
|
+
type={onClick ? 'button' : 'submit'}
|
|
29
|
+
>
|
|
30
|
+
{children}
|
|
31
|
+
</button>
|
|
32
|
+
);
|
|
33
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { ArrowRight, TriangleAlert } from '@tuturuuu/icons';
|
|
4
|
+
import type { InventoryStorefront } from '@tuturuuu/internal-api/inventory';
|
|
5
|
+
import { cn } from '@tuturuuu/utils/format';
|
|
6
|
+
import type { FormEvent } from 'react';
|
|
7
|
+
import { Badge } from '../badge';
|
|
8
|
+
import { Button } from '../button';
|
|
9
|
+
import { AccentButton } from './accent-button';
|
|
10
|
+
import type { StorefrontCartEntry, StorefrontSurfaceLabels } from './types';
|
|
11
|
+
import { formatStorefrontPrice, storefrontSurfaceClasses } from './utils';
|
|
12
|
+
|
|
13
|
+
export function StorefrontCartSummary({
|
|
14
|
+
cartEntries,
|
|
15
|
+
checkoutHref,
|
|
16
|
+
currency,
|
|
17
|
+
isCheckout,
|
|
18
|
+
isPreview,
|
|
19
|
+
isSubmitting,
|
|
20
|
+
labels,
|
|
21
|
+
onCheckoutSubmit,
|
|
22
|
+
radius,
|
|
23
|
+
storefront,
|
|
24
|
+
total,
|
|
25
|
+
}: {
|
|
26
|
+
cartEntries: StorefrontCartEntry[];
|
|
27
|
+
checkoutHref?: string;
|
|
28
|
+
currency: string;
|
|
29
|
+
isCheckout: boolean;
|
|
30
|
+
isPreview: boolean;
|
|
31
|
+
isSubmitting: boolean;
|
|
32
|
+
labels: StorefrontSurfaceLabels;
|
|
33
|
+
onCheckoutSubmit?: (formData: FormData) => void;
|
|
34
|
+
radius: string;
|
|
35
|
+
storefront: InventoryStorefront;
|
|
36
|
+
total: number;
|
|
37
|
+
}) {
|
|
38
|
+
const hasCart = cartEntries.length > 0;
|
|
39
|
+
const isCheckoutDisabled = storefront.checkoutMode === 'disabled';
|
|
40
|
+
const submitDisabled = !hasCart || isSubmitting || isCheckoutDisabled;
|
|
41
|
+
|
|
42
|
+
return (
|
|
43
|
+
<aside
|
|
44
|
+
className={cn(
|
|
45
|
+
'h-fit p-4 lg:sticky lg:top-4',
|
|
46
|
+
storefrontSurfaceClasses[storefront.surfaceStyle],
|
|
47
|
+
radius
|
|
48
|
+
)}
|
|
49
|
+
>
|
|
50
|
+
<div className="flex items-center justify-between gap-3">
|
|
51
|
+
<p className="font-semibold">{labels.cart}</p>
|
|
52
|
+
<Badge className="border-border bg-background" variant="outline">
|
|
53
|
+
{cartEntries.length}
|
|
54
|
+
</Badge>
|
|
55
|
+
</div>
|
|
56
|
+
<p className="mt-2 text-muted-foreground text-sm leading-6">
|
|
57
|
+
{labels.reservedCopy}
|
|
58
|
+
</p>
|
|
59
|
+
<div className="mt-4 grid gap-2">
|
|
60
|
+
{cartEntries.map(({ line, listing }) => (
|
|
61
|
+
<div
|
|
62
|
+
className="flex items-center justify-between gap-3 text-sm"
|
|
63
|
+
key={line.listingId}
|
|
64
|
+
>
|
|
65
|
+
<span className="min-w-0 truncate">
|
|
66
|
+
{line.quantity} x {listing.title}
|
|
67
|
+
</span>
|
|
68
|
+
<span className="font-medium">
|
|
69
|
+
{formatStorefrontPrice(listing.price * line.quantity, currency)}
|
|
70
|
+
</span>
|
|
71
|
+
</div>
|
|
72
|
+
))}
|
|
73
|
+
</div>
|
|
74
|
+
<div className="mt-4 flex items-center justify-between border-border border-t pt-4">
|
|
75
|
+
<span className="text-muted-foreground text-sm">{labels.total}</span>
|
|
76
|
+
<span className="font-semibold">
|
|
77
|
+
{formatStorefrontPrice(total, currency)}
|
|
78
|
+
</span>
|
|
79
|
+
</div>
|
|
80
|
+
{!hasCart ? (
|
|
81
|
+
<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">
|
|
82
|
+
<TriangleAlert className="h-4 w-4" />
|
|
83
|
+
{labels.emptyCart}
|
|
84
|
+
</p>
|
|
85
|
+
) : null}
|
|
86
|
+
{isCheckout ? (
|
|
87
|
+
<form
|
|
88
|
+
className="mt-4 grid gap-2"
|
|
89
|
+
onSubmit={(event: FormEvent<HTMLFormElement>) => {
|
|
90
|
+
event.preventDefault();
|
|
91
|
+
onCheckoutSubmit?.(new FormData(event.currentTarget));
|
|
92
|
+
}}
|
|
93
|
+
>
|
|
94
|
+
<input
|
|
95
|
+
className="h-10 rounded-md border border-input bg-background px-3 text-sm outline-none focus-visible:ring-2 focus-visible:ring-ring/40"
|
|
96
|
+
name="name"
|
|
97
|
+
placeholder={labels.form.name}
|
|
98
|
+
required
|
|
99
|
+
/>
|
|
100
|
+
<input
|
|
101
|
+
className="h-10 rounded-md border border-input bg-background px-3 text-sm outline-none focus-visible:ring-2 focus-visible:ring-ring/40"
|
|
102
|
+
name="email"
|
|
103
|
+
placeholder={labels.form.email}
|
|
104
|
+
required
|
|
105
|
+
type="email"
|
|
106
|
+
/>
|
|
107
|
+
<input
|
|
108
|
+
className="h-10 rounded-md border border-input bg-background px-3 text-sm outline-none focus-visible:ring-2 focus-visible:ring-ring/40"
|
|
109
|
+
name="phone"
|
|
110
|
+
placeholder={labels.form.phone}
|
|
111
|
+
/>
|
|
112
|
+
<textarea
|
|
113
|
+
className="min-h-20 rounded-md border border-input bg-background px-3 py-2 text-sm outline-none focus-visible:ring-2 focus-visible:ring-ring/40"
|
|
114
|
+
name="note"
|
|
115
|
+
placeholder={labels.form.note}
|
|
116
|
+
/>
|
|
117
|
+
<AccentButton disabled={submitDisabled} radius={radius}>
|
|
118
|
+
{isSubmitting ? labels.reserving : labels.reserve}
|
|
119
|
+
<ArrowRight className="h-4 w-4" />
|
|
120
|
+
</AccentButton>
|
|
121
|
+
</form>
|
|
122
|
+
) : isPreview || isCheckoutDisabled ? (
|
|
123
|
+
<Button className={cn('mt-4 w-full', radius)} disabled type="button">
|
|
124
|
+
{labels.checkoutDisabled}
|
|
125
|
+
</Button>
|
|
126
|
+
) : (
|
|
127
|
+
<Button
|
|
128
|
+
asChild
|
|
129
|
+
className={cn('mt-4 w-full', radius)}
|
|
130
|
+
disabled={!hasCart}
|
|
131
|
+
>
|
|
132
|
+
<a aria-disabled={!hasCart} href={hasCart ? checkoutHref : undefined}>
|
|
133
|
+
{labels.checkout}
|
|
134
|
+
<ArrowRight className="h-4 w-4" />
|
|
135
|
+
</a>
|
|
136
|
+
</Button>
|
|
137
|
+
)}
|
|
138
|
+
</aside>
|
|
139
|
+
);
|
|
140
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { PackageOpen } from '@tuturuuu/icons';
|
|
2
|
+
import { cn } from '@tuturuuu/utils/format';
|
|
3
|
+
import type { ReactNode } from 'react';
|
|
4
|
+
import type { StorefrontSurfaceLabels } from './types';
|
|
5
|
+
|
|
6
|
+
export function StorefrontEmptyListings({
|
|
7
|
+
action,
|
|
8
|
+
labels,
|
|
9
|
+
radius,
|
|
10
|
+
}: {
|
|
11
|
+
action?: ReactNode;
|
|
12
|
+
labels: StorefrontSurfaceLabels;
|
|
13
|
+
radius: string;
|
|
14
|
+
}) {
|
|
15
|
+
return (
|
|
16
|
+
<div
|
|
17
|
+
className={cn(
|
|
18
|
+
'grid min-h-56 place-items-center border border-dashed bg-muted/25 p-6 text-center sm:col-span-2 xl:col-span-3',
|
|
19
|
+
radius
|
|
20
|
+
)}
|
|
21
|
+
>
|
|
22
|
+
<div className="max-w-sm">
|
|
23
|
+
<PackageOpen className="mx-auto h-8 w-8 text-muted-foreground" />
|
|
24
|
+
<p className="mt-3 font-semibold">{labels.emptyListingsTitle}</p>
|
|
25
|
+
<p className="mt-1 text-muted-foreground text-sm leading-6">
|
|
26
|
+
{labels.emptyListingsDescription}
|
|
27
|
+
</p>
|
|
28
|
+
{action ? <div className="mt-4">{action}</div> : null}
|
|
29
|
+
</div>
|
|
30
|
+
</div>
|
|
31
|
+
);
|
|
32
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { Store } from '@tuturuuu/icons';
|
|
2
|
+
import type { InventoryStorefront } from '@tuturuuu/internal-api/inventory';
|
|
3
|
+
import { cn } from '@tuturuuu/utils/format';
|
|
4
|
+
import { Badge } from '../badge';
|
|
5
|
+
import { StorefrontImagePanel } from './image-panel';
|
|
6
|
+
import type { StorefrontSurfaceLabels } from './types';
|
|
7
|
+
import { storefrontSurfaceClasses } from './utils';
|
|
8
|
+
|
|
9
|
+
export function StorefrontHeroPanel({
|
|
10
|
+
currency,
|
|
11
|
+
labels,
|
|
12
|
+
listingsCount,
|
|
13
|
+
radius,
|
|
14
|
+
storefront,
|
|
15
|
+
}: {
|
|
16
|
+
currency: string;
|
|
17
|
+
labels: StorefrontSurfaceLabels;
|
|
18
|
+
listingsCount: number;
|
|
19
|
+
radius: string;
|
|
20
|
+
storefront: InventoryStorefront;
|
|
21
|
+
}) {
|
|
22
|
+
return (
|
|
23
|
+
<section
|
|
24
|
+
className={cn(
|
|
25
|
+
'grid min-h-44 overflow-hidden',
|
|
26
|
+
storefrontSurfaceClasses[storefront.surfaceStyle],
|
|
27
|
+
radius,
|
|
28
|
+
storefront.themePreset === 'editorial'
|
|
29
|
+
? 'md:grid-cols-[minmax(0,1.15fr)_360px]'
|
|
30
|
+
: 'md:grid-cols-[minmax(0,1fr)_280px]'
|
|
31
|
+
)}
|
|
32
|
+
>
|
|
33
|
+
<div className="flex min-w-0 flex-col justify-between gap-6 p-5">
|
|
34
|
+
<div>
|
|
35
|
+
<div className="flex items-center gap-2 text-muted-foreground text-xs">
|
|
36
|
+
<Store className="h-4 w-4" />
|
|
37
|
+
<span>{labels.browse}</span>
|
|
38
|
+
</div>
|
|
39
|
+
<h2
|
|
40
|
+
className={cn(
|
|
41
|
+
'mt-3 text-balance font-semibold tracking-normal',
|
|
42
|
+
storefront.themePreset === 'editorial'
|
|
43
|
+
? 'text-3xl md:text-4xl'
|
|
44
|
+
: 'text-2xl'
|
|
45
|
+
)}
|
|
46
|
+
>
|
|
47
|
+
{storefront.name}
|
|
48
|
+
</h2>
|
|
49
|
+
<p className="mt-2 max-w-2xl text-muted-foreground text-sm leading-6">
|
|
50
|
+
{storefront.description ?? labels.fallbackDescription}
|
|
51
|
+
</p>
|
|
52
|
+
</div>
|
|
53
|
+
<div className="flex flex-wrap gap-2">
|
|
54
|
+
<Badge className="border-border bg-background" variant="outline">
|
|
55
|
+
{listingsCount} {labels.product}
|
|
56
|
+
</Badge>
|
|
57
|
+
<Badge className="border-border bg-background" variant="outline">
|
|
58
|
+
{currency}
|
|
59
|
+
</Badge>
|
|
60
|
+
</div>
|
|
61
|
+
</div>
|
|
62
|
+
|
|
63
|
+
<StorefrontImagePanel
|
|
64
|
+
className="min-h-44 md:min-h-full"
|
|
65
|
+
imageUrl={storefront.heroImageUrl}
|
|
66
|
+
label={storefront.name}
|
|
67
|
+
/>
|
|
68
|
+
</section>
|
|
69
|
+
);
|
|
70
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { PackageOpen } from '@tuturuuu/icons';
|
|
2
|
+
import { cn } from '@tuturuuu/utils/format';
|
|
3
|
+
import { getListingInitials } from './utils';
|
|
4
|
+
|
|
5
|
+
export function StorefrontImagePanel({
|
|
6
|
+
className,
|
|
7
|
+
imageUrl,
|
|
8
|
+
label,
|
|
9
|
+
}: {
|
|
10
|
+
className?: string;
|
|
11
|
+
imageUrl: string | null;
|
|
12
|
+
label: string;
|
|
13
|
+
}) {
|
|
14
|
+
if (imageUrl) {
|
|
15
|
+
return (
|
|
16
|
+
// biome-ignore lint/performance/noImgElement: storefront images are workspace-controlled external URLs
|
|
17
|
+
<img
|
|
18
|
+
alt=""
|
|
19
|
+
className={cn('w-full object-cover', className)}
|
|
20
|
+
src={imageUrl}
|
|
21
|
+
/>
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return (
|
|
26
|
+
<div
|
|
27
|
+
className={cn(
|
|
28
|
+
'grid w-full place-items-center border-border bg-muted/55 text-muted-foreground',
|
|
29
|
+
className
|
|
30
|
+
)}
|
|
31
|
+
>
|
|
32
|
+
<div className="grid place-items-center gap-2 text-center">
|
|
33
|
+
<PackageOpen className="h-6 w-6" />
|
|
34
|
+
<span className="max-w-24 truncate font-semibold text-sm">
|
|
35
|
+
{getListingInitials(label) || label}
|
|
36
|
+
</span>
|
|
37
|
+
</div>
|
|
38
|
+
</div>
|
|
39
|
+
);
|
|
40
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export { StorefrontSurface } from './storefront-surface';
|
|
2
|
+
export type {
|
|
3
|
+
StorefrontCartEntry,
|
|
4
|
+
StorefrontCartLine,
|
|
5
|
+
StorefrontSurfaceLabels,
|
|
6
|
+
StorefrontSurfaceMode,
|
|
7
|
+
} from './types';
|
|
8
|
+
export {
|
|
9
|
+
formatStorefrontPrice,
|
|
10
|
+
getStorefrontListingLimit,
|
|
11
|
+
sanitizeStorefrontAccentColor,
|
|
12
|
+
} from './utils';
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { Minus, Plus } from '@tuturuuu/icons';
|
|
4
|
+
import type { InventoryStorefrontListing } from '@tuturuuu/internal-api/inventory';
|
|
5
|
+
import { cn } from '@tuturuuu/utils/format';
|
|
6
|
+
import { Badge } from '../badge';
|
|
7
|
+
import { Button } from '../button';
|
|
8
|
+
import { AccentButton } from './accent-button';
|
|
9
|
+
import { StorefrontImagePanel } from './image-panel';
|
|
10
|
+
import type { StorefrontSurfaceLabels } from './types';
|
|
11
|
+
import { formatStorefrontPrice, getStorefrontListingLimit } from './utils';
|
|
12
|
+
|
|
13
|
+
export function StorefrontListingCard({
|
|
14
|
+
currency,
|
|
15
|
+
isList,
|
|
16
|
+
labels,
|
|
17
|
+
listing,
|
|
18
|
+
onDecrement,
|
|
19
|
+
onIncrement,
|
|
20
|
+
quantity,
|
|
21
|
+
radius,
|
|
22
|
+
showInventoryBadges,
|
|
23
|
+
surfaceClassName,
|
|
24
|
+
}: {
|
|
25
|
+
currency: string;
|
|
26
|
+
isList: boolean;
|
|
27
|
+
labels: StorefrontSurfaceLabels;
|
|
28
|
+
listing: InventoryStorefrontListing;
|
|
29
|
+
onDecrement?: (listingId: string) => void;
|
|
30
|
+
onIncrement?: (listingId: string, maxQuantity: number) => void;
|
|
31
|
+
quantity: number;
|
|
32
|
+
radius: string;
|
|
33
|
+
showInventoryBadges: boolean;
|
|
34
|
+
surfaceClassName: string;
|
|
35
|
+
}) {
|
|
36
|
+
const limit = getStorefrontListingLimit(listing);
|
|
37
|
+
const disabled = limit === 0 || quantity >= limit;
|
|
38
|
+
const canChange = Boolean(onIncrement || onDecrement);
|
|
39
|
+
|
|
40
|
+
return (
|
|
41
|
+
<article
|
|
42
|
+
className={cn(
|
|
43
|
+
surfaceClassName,
|
|
44
|
+
radius,
|
|
45
|
+
isList
|
|
46
|
+
? 'grid gap-3 p-3 sm:grid-cols-[112px_minmax(0,1fr)_auto] sm:items-center'
|
|
47
|
+
: 'grid min-h-full gap-4 p-3'
|
|
48
|
+
)}
|
|
49
|
+
>
|
|
50
|
+
<StorefrontImagePanel
|
|
51
|
+
className={cn(isList ? 'aspect-square' : 'aspect-[4/3]')}
|
|
52
|
+
imageUrl={listing.imageUrl}
|
|
53
|
+
label={listing.title}
|
|
54
|
+
/>
|
|
55
|
+
<div className="min-w-0">
|
|
56
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
57
|
+
<p className="min-w-0 truncate font-semibold">{listing.title}</p>
|
|
58
|
+
<Badge className="border-border bg-background" variant="outline">
|
|
59
|
+
{listing.listingType === 'bundle' ? labels.bundle : labels.product}
|
|
60
|
+
</Badge>
|
|
61
|
+
</div>
|
|
62
|
+
<p className="mt-1 line-clamp-2 text-muted-foreground text-sm leading-6">
|
|
63
|
+
{listing.description ?? labels.fallbackDescription}
|
|
64
|
+
</p>
|
|
65
|
+
{showInventoryBadges ? (
|
|
66
|
+
<p className="mt-2 text-muted-foreground text-xs">
|
|
67
|
+
{limit === 0
|
|
68
|
+
? labels.soldOut
|
|
69
|
+
: `${listing.availableQuantity ?? labels.available} ${
|
|
70
|
+
typeof listing.availableQuantity === 'number'
|
|
71
|
+
? labels.available
|
|
72
|
+
: ''
|
|
73
|
+
}`.trim()}
|
|
74
|
+
</p>
|
|
75
|
+
) : null}
|
|
76
|
+
</div>
|
|
77
|
+
|
|
78
|
+
<div className="mt-auto flex flex-wrap items-center justify-between gap-3">
|
|
79
|
+
<div>
|
|
80
|
+
<p className="font-semibold">
|
|
81
|
+
{formatStorefrontPrice(listing.price, currency)}
|
|
82
|
+
</p>
|
|
83
|
+
{listing.compareAtPrice ? (
|
|
84
|
+
<p className="text-muted-foreground text-xs line-through">
|
|
85
|
+
{formatStorefrontPrice(listing.compareAtPrice, currency)}
|
|
86
|
+
</p>
|
|
87
|
+
) : null}
|
|
88
|
+
</div>
|
|
89
|
+
{canChange ? (
|
|
90
|
+
quantity > 0 ? (
|
|
91
|
+
<div className="flex items-center gap-1">
|
|
92
|
+
<Button
|
|
93
|
+
aria-label={`${labels.quantity} -`}
|
|
94
|
+
className={cn('h-8 w-8 p-0', radius)}
|
|
95
|
+
onClick={() => onDecrement?.(listing.id)}
|
|
96
|
+
type="button"
|
|
97
|
+
variant="outline"
|
|
98
|
+
>
|
|
99
|
+
<Minus className="h-4 w-4" />
|
|
100
|
+
</Button>
|
|
101
|
+
<span className="min-w-8 text-center font-medium text-sm">
|
|
102
|
+
{quantity}
|
|
103
|
+
</span>
|
|
104
|
+
<Button
|
|
105
|
+
aria-label={`${labels.quantity} +`}
|
|
106
|
+
className={cn('h-8 w-8 p-0', radius)}
|
|
107
|
+
disabled={disabled}
|
|
108
|
+
onClick={() => onIncrement?.(listing.id, limit)}
|
|
109
|
+
type="button"
|
|
110
|
+
variant="outline"
|
|
111
|
+
>
|
|
112
|
+
<Plus className="h-4 w-4" />
|
|
113
|
+
</Button>
|
|
114
|
+
</div>
|
|
115
|
+
) : (
|
|
116
|
+
<AccentButton
|
|
117
|
+
disabled={disabled}
|
|
118
|
+
onClick={() => onIncrement?.(listing.id, limit)}
|
|
119
|
+
radius={radius}
|
|
120
|
+
>
|
|
121
|
+
<Plus className="h-4 w-4" />
|
|
122
|
+
{limit === 0 ? labels.soldOut : labels.add}
|
|
123
|
+
</AccentButton>
|
|
124
|
+
)
|
|
125
|
+
) : null}
|
|
126
|
+
</div>
|
|
127
|
+
</article>
|
|
128
|
+
);
|
|
129
|
+
}
|