@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.
Files changed (107) hide show
  1. package/CHANGELOG.md +43 -0
  2. package/package.json +41 -34
  3. package/src/components/ui/currency-input.tsx +65 -23
  4. package/src/components/ui/custom/__tests__/sidebar-context.test.tsx +64 -0
  5. package/src/components/ui/custom/__tests__/sidebar-remote-behavior-bridge.test.tsx +109 -0
  6. package/src/components/ui/custom/combobox.test.tsx +141 -0
  7. package/src/components/ui/custom/combobox.tsx +105 -36
  8. package/src/components/ui/custom/settings/task-settings.tsx +126 -0
  9. package/src/components/ui/custom/settings/task-sound-settings.test.tsx +146 -0
  10. package/src/components/ui/custom/sidebar-context.tsx +68 -6
  11. package/src/components/ui/custom/sidebar-remote-behavior-bridge.tsx +21 -2
  12. package/src/components/ui/finance/finance-layout.tsx +2 -4
  13. package/src/components/ui/finance/shared/balance-mode-toggle.tsx +35 -0
  14. package/src/components/ui/finance/shared/finance-layout-controls.tsx +43 -0
  15. package/src/components/ui/finance/shared/quick-actions.tsx +14 -6
  16. package/src/components/ui/finance/shared/use-finance-balance-mode.ts +72 -0
  17. package/src/components/ui/finance/shared/wallet-balance-mode.test.ts +66 -0
  18. package/src/components/ui/finance/shared/wallet-balance-mode.ts +42 -0
  19. package/src/components/ui/finance/transactions/form-types.ts +23 -0
  20. package/src/components/ui/finance/transactions/form.tsx +81 -22
  21. package/src/components/ui/finance/transactions/infinite-transactions-list.tsx +29 -18
  22. package/src/components/ui/finance/transactions/transaction-card.tsx +75 -43
  23. package/src/components/ui/finance/transactions/transfer-merge.test.ts +90 -0
  24. package/src/components/ui/finance/transactions/transfer-merge.ts +52 -0
  25. package/src/components/ui/finance/transactions/wallet-filter.tsx +21 -2
  26. package/src/components/ui/finance/wallets/checkpoints/wallet-checkpoint-adjustment-dialog.tsx +219 -0
  27. package/src/components/ui/finance/wallets/checkpoints/wallet-checkpoint-amount.tsx +32 -0
  28. package/src/components/ui/finance/wallets/checkpoints/wallet-checkpoint-delete-dialog.tsx +50 -0
  29. package/src/components/ui/finance/wallets/checkpoints/wallet-checkpoint-dialog.tsx +138 -0
  30. package/src/components/ui/finance/wallets/checkpoints/wallet-checkpoint-history-dialog.tsx +617 -0
  31. package/src/components/ui/finance/wallets/checkpoints/wallet-checkpoint-panel.tsx +197 -0
  32. package/src/components/ui/finance/wallets/checkpoints/wallet-checkpoint-sections.tsx +201 -0
  33. package/src/components/ui/finance/wallets/checkpoints/wallet-checkpoints.test.tsx +541 -0
  34. package/src/components/ui/finance/wallets/checkpoints/wallet-total-check-dialog.tsx +362 -0
  35. package/src/components/ui/finance/wallets/columns-rendering.test.tsx +125 -0
  36. package/src/components/ui/finance/wallets/columns.test.ts +56 -0
  37. package/src/components/ui/finance/wallets/columns.tsx +196 -43
  38. package/src/components/ui/finance/wallets/form.test.tsx +79 -14
  39. package/src/components/ui/finance/wallets/form.tsx +41 -197
  40. package/src/components/ui/finance/wallets/query-invalidation.ts +3 -0
  41. package/src/components/ui/finance/wallets/wallet-basics-fields.tsx +141 -0
  42. package/src/components/ui/finance/wallets/wallet-credit-fields.tsx +136 -0
  43. package/src/components/ui/finance/wallets/walletId/credit-wallet-summary.tsx +143 -68
  44. package/src/components/ui/finance/wallets/walletId/wallet-details-actions.test.tsx +105 -0
  45. package/src/components/ui/finance/wallets/walletId/wallet-details-actions.tsx +120 -16
  46. package/src/components/ui/finance/wallets/walletId/wallet-details-amount.test.tsx +64 -0
  47. package/src/components/ui/finance/wallets/walletId/wallet-details-amount.tsx +226 -6
  48. package/src/components/ui/finance/wallets/walletId/wallet-details-page.test.tsx +71 -5
  49. package/src/components/ui/finance/wallets/walletId/wallet-details-page.tsx +52 -35
  50. package/src/components/ui/finance/wallets/wallets-data-table.test.tsx +171 -0
  51. package/src/components/ui/finance/wallets/wallets-data-table.tsx +132 -29
  52. package/src/components/ui/finance/wallets/wallets-page.test.tsx +117 -36
  53. package/src/components/ui/finance/wallets/wallets-page.tsx +40 -64
  54. package/src/components/ui/storefront/accent-button.tsx +33 -0
  55. package/src/components/ui/storefront/cart-summary.tsx +140 -0
  56. package/src/components/ui/storefront/empty-listings.tsx +32 -0
  57. package/src/components/ui/storefront/hero-panel.tsx +70 -0
  58. package/src/components/ui/storefront/image-panel.tsx +40 -0
  59. package/src/components/ui/storefront/index.ts +12 -0
  60. package/src/components/ui/storefront/listing-card.tsx +129 -0
  61. package/src/components/ui/storefront/storefront-surface.test.tsx +85 -0
  62. package/src/components/ui/storefront/storefront-surface.tsx +235 -0
  63. package/src/components/ui/storefront/types.ts +99 -0
  64. package/src/components/ui/storefront/utils.ts +90 -0
  65. package/src/components/ui/tu-do/boards/boardId/kanban/bulk/__tests__/bulk-mutations-move.test.tsx +14 -0
  66. package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-operations.ts +29 -0
  67. package/src/components/ui/tu-do/boards/boardId/task-card/task-card-open-options.test.ts +134 -0
  68. package/src/components/ui/tu-do/boards/boardId/task-card/task-card-open-options.ts +127 -0
  69. package/src/components/ui/tu-do/boards/boardId/task-card/task-card.tsx +17 -42
  70. package/src/components/ui/tu-do/boards/boardId/timeline-board-open-task.test.tsx +164 -0
  71. package/src/components/ui/tu-do/boards/boardId/timeline-board.tsx +25 -16
  72. package/src/components/ui/tu-do/hooks/useTaskDialog.ts +15 -1
  73. package/src/components/ui/tu-do/my-tasks/__tests__/use-task-context-actions.test.ts +11 -0
  74. package/src/components/ui/tu-do/my-tasks/use-my-tasks-state.ts +2 -0
  75. package/src/components/ui/tu-do/my-tasks/use-task-context-actions.ts +124 -7
  76. package/src/components/ui/tu-do/providers/__tests__/task-dialog-provider.test.tsx +217 -5
  77. package/src/components/ui/tu-do/providers/task-dialog-provider.tsx +180 -35
  78. package/src/components/ui/tu-do/shared/__tests__/task-dialog-manager.test.tsx +222 -26
  79. package/src/components/ui/tu-do/shared/board-client.tsx +1 -3
  80. package/src/components/ui/tu-do/shared/list-view-context-menu.test.tsx +55 -2
  81. package/src/components/ui/tu-do/shared/list-view.tsx +23 -16
  82. package/src/components/ui/tu-do/shared/task-dialog-manager.tsx +93 -76
  83. package/src/components/ui/tu-do/shared/task-dialog-presentation.ts +11 -0
  84. package/src/components/ui/tu-do/shared/task-edit-dialog/components/compact-task-create-popover.test.tsx +268 -0
  85. package/src/components/ui/tu-do/shared/task-edit-dialog/components/compact-task-create-popover.tsx +243 -0
  86. package/src/components/ui/tu-do/shared/task-edit-dialog/components/quick-settings-popover.tsx +26 -0
  87. package/src/components/ui/tu-do/shared/task-edit-dialog/components/smart-task-suggestions-panel.test.tsx +129 -0
  88. package/src/components/ui/tu-do/shared/task-edit-dialog/components/smart-task-suggestions-panel.tsx +358 -0
  89. package/src/components/ui/tu-do/shared/task-edit-dialog/components/task-description-editor.tsx +1 -1
  90. package/src/components/ui/tu-do/shared/task-edit-dialog/components/task-dialog-header.tsx +6 -2
  91. package/src/components/ui/tu-do/shared/task-edit-dialog/components/task-list-selector.tsx +36 -20
  92. package/src/components/ui/tu-do/shared/task-edit-dialog/components/task-name-input.test.tsx +41 -1
  93. package/src/components/ui/tu-do/shared/task-edit-dialog/components/task-name-input.tsx +157 -102
  94. package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/use-task-form-reset.ts +18 -2
  95. package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/use-task-realtime-sync.ts +1 -2
  96. package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/use-task-save.test.ts +84 -1
  97. package/src/components/ui/tu-do/shared/task-edit-dialog/hooks/use-task-save.ts +5 -1
  98. package/src/components/ui/tu-do/shared/task-edit-dialog/task-dialog-actions.tsx +5 -3
  99. package/src/components/ui/tu-do/shared/task-edit-dialog/task-properties-section.tsx +300 -172
  100. package/src/components/ui/tu-do/shared/task-edit-dialog.tsx +959 -340
  101. package/src/components/ui/tu-do/shared/task-sound-effects.test.ts +189 -0
  102. package/src/components/ui/tu-do/shared/task-sound-effects.tsx +468 -0
  103. package/src/hooks/__tests__/use-task-actions.test.tsx +61 -0
  104. package/src/hooks/use-task-actions.ts +45 -0
  105. package/src/hooks/useBoardRealtime.ts +54 -1
  106. package/src/hooks/useBoardRealtimeEventHandler.ts +169 -4
  107. 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: string;
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 resolvedInternalApiOptions =
69
- internalApiOptions ?? withForwardedInternalApiAuth(await headers());
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={openCreateDialog || searchParams.create === 'wallet'}
91
- form={canCreateWallets ? <WalletForm wsId={wsId} /> : undefined}
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
- page={page}
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
+ }