@carlonicora/nextjs-jsonapi 4.1.1 → 4.2.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/dist/{AssistantMessageInterface-CubxuBIJ.d.ts → AssistantMessageInterface-DHJJO_Z_.d.ts} +1 -1
- package/dist/{AssistantMessageInterface-sHjI2zPN.d.mts → AssistantMessageInterface-S7oyERTT.d.mts} +1 -1
- package/dist/{BlockNoteEditor-VOYKZOP5.mjs → BlockNoteEditor-HXAHDJA7.mjs} +34 -27
- package/dist/BlockNoteEditor-HXAHDJA7.mjs.map +1 -0
- package/dist/{BlockNoteEditor-BENYTTCZ.js → BlockNoteEditor-LLCZDFLZ.js} +50 -43
- package/dist/BlockNoteEditor-LLCZDFLZ.js.map +1 -0
- package/dist/{BlockNoteViewer-USLR4Y7C.js → BlockNoteViewer-77CCTC2U.js} +3 -3
- package/dist/{BlockNoteViewer-USLR4Y7C.js.map → BlockNoteViewer-77CCTC2U.js.map} +1 -1
- package/dist/{BlockNoteViewer-Y2ZM4ESA.mjs → BlockNoteViewer-MDPO6BXN.mjs} +2 -2
- package/dist/{auth.interface-Df4vXbNz.d.mts → auth.interface-CeFCz9NU.d.mts} +1 -1
- package/dist/{auth.interface-BzI4onI_.d.ts → auth.interface-D1R7VVky.d.ts} +1 -1
- package/dist/billing/index.d.mts +40 -6
- package/dist/billing/index.d.ts +40 -6
- package/dist/billing/index.js +806 -774
- package/dist/billing/index.js.map +1 -1
- package/dist/billing/index.mjs +494 -462
- package/dist/billing/index.mjs.map +1 -1
- package/dist/{chunk-6ISF5KNQ.mjs → chunk-2VXDZRYY.mjs} +1103 -816
- package/dist/chunk-2VXDZRYY.mjs.map +1 -0
- package/dist/{chunk-4BZJ4LAB.js → chunk-4ORLPEK7.js} +136 -104
- package/dist/chunk-4ORLPEK7.js.map +1 -0
- package/dist/{chunk-ZZ6E33FO.mjs → chunk-6DRSSW5O.mjs} +36 -4
- package/dist/chunk-6DRSSW5O.mjs.map +1 -0
- package/dist/{chunk-KS6WZP2E.js → chunk-6SMJRAE3.js} +4 -4
- package/dist/{chunk-KS6WZP2E.js.map → chunk-6SMJRAE3.js.map} +1 -1
- package/dist/{chunk-WQ77CUUZ.js → chunk-GPXYQMOZ.js} +7 -7
- package/dist/{chunk-WQ77CUUZ.js.map → chunk-GPXYQMOZ.js.map} +1 -1
- package/dist/{chunk-46S3KPLU.mjs → chunk-ILFFC6BG.mjs} +2 -2
- package/dist/{chunk-HZVVRIJQ.mjs → chunk-IUS2RHCX.mjs} +2 -2
- package/dist/{chunk-F7RNBGHS.js → chunk-KJMMBUOA.js} +3 -3
- package/dist/{chunk-F7RNBGHS.js.map → chunk-KJMMBUOA.js.map} +1 -1
- package/dist/{chunk-ZO3QQ5CQ.mjs → chunk-MQDR6MEE.mjs} +2 -2
- package/dist/{chunk-ORU7KLCY.js → chunk-VDVIUDY5.js} +1445 -1158
- package/dist/chunk-VDVIUDY5.js.map +1 -0
- package/dist/client/index.d.mts +20 -6
- package/dist/client/index.d.ts +20 -6
- package/dist/client/index.js +6 -4
- package/dist/client/index.js.map +1 -1
- package/dist/client/index.mjs +5 -3
- package/dist/components/index.d.mts +59 -21
- package/dist/components/index.d.ts +59 -21
- package/dist/components/index.js +4 -4
- package/dist/components/index.mjs +3 -3
- package/dist/{config-CEAq4Ozy.d.mts → config-BP2vyddw.d.mts} +5 -0
- package/dist/{config-DydDHCfb.d.ts → config-BhW2QrRD.d.ts} +5 -0
- package/dist/contexts/index.d.mts +4 -4
- package/dist/contexts/index.d.ts +4 -4
- package/dist/contexts/index.js +4 -4
- package/dist/contexts/index.mjs +3 -3
- package/dist/core/index.d.mts +17 -9
- package/dist/core/index.d.ts +17 -9
- package/dist/core/index.js +4 -2
- package/dist/core/index.js.map +1 -1
- package/dist/core/index.mjs +3 -1
- package/dist/features/analytics/index.d.mts +1 -1
- package/dist/features/analytics/index.d.ts +1 -1
- package/dist/features/analytics/index.js +122 -122
- package/dist/features/analytics/index.mjs +4 -4
- package/dist/features/analytics/server.js +4 -4
- package/dist/features/analytics/server.mjs +2 -2
- package/dist/features/help/index.d.mts +1 -1
- package/dist/features/help/index.d.ts +1 -1
- package/dist/features/help/index.js +42 -40
- package/dist/features/help/index.js.map +1 -1
- package/dist/features/help/index.mjs +8 -6
- package/dist/features/help/index.mjs.map +1 -1
- package/dist/features/tokenusage/index.js +125 -125
- package/dist/features/tokenusage/index.mjs +4 -4
- package/dist/index.d.mts +7 -7
- package/dist/index.d.ts +7 -7
- package/dist/index.js +5 -3
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +4 -2
- package/dist/{notification.interface-P_pNfzZQ.d.mts → notification.interface-BB6GPpZY.d.mts} +1 -1
- package/dist/{notification.interface-BNCvnVCW.d.ts → notification.interface-CmAGrijJ.d.ts} +1 -1
- package/dist/{s3.service-CbUa6a-8.d.ts → s3.service-C-4YpEMu.d.ts} +7 -3
- package/dist/{s3.service-DAp8FbuG.d.mts → s3.service-dVj-nxMl.d.mts} +7 -3
- package/dist/server/index.d.mts +4 -4
- package/dist/server/index.d.ts +4 -4
- package/dist/server/index.js +3 -3
- package/dist/server/index.mjs +2 -2
- package/dist/{useSocket-BHVIyigI.d.ts → useSocket-B0H9pmmH.d.ts} +1 -1
- package/dist/{useSocket-B9gTbuCj.d.mts → useSocket-B5RQb6v2.d.mts} +1 -1
- package/dist/{user.interface-CrmwJLYW.d.ts → user.interface-C0ZdsBZj.d.ts} +2 -0
- package/dist/{user.interface-CSaGWFbg.d.mts → user.interface-CIWGlbqM.d.mts} +2 -0
- package/package.json +1 -1
- package/scripts/vitest-slow-tests.reporter.spec.ts +54 -0
- package/src/components/containers/RoundPageContainer.tsx +3 -1
- package/src/components/containers/RoundPageContainerTitle.tsx +5 -2
- package/src/components/editors/BlockNoteEditor.tsx +31 -15
- package/src/components/editors/BlockNoteEditorAiActions.tsx +11 -7
- package/src/components/editors/__tests__/BlockNoteEditorAiActions.test.ts +10 -3
- package/src/components/errors/ErrorDetails.tsx +5 -2
- package/src/components/forms/CommonAssociationForm.tsx +1 -1
- package/src/components/forms/EntityMultiSelector.tsx +6 -2
- package/src/components/forms/EntitySelector.tsx +4 -1
- package/src/components/forms/FileUploader.tsx +3 -2
- package/src/components/forms/MultiFileUploader.tsx +3 -1
- package/src/components/navigations/Breadcrumb.tsx +3 -2
- package/src/components/navigations/Header.tsx +3 -1
- package/src/components/navigations/MobileNavigationBar.tsx +3 -1
- package/src/components/navigations/__tests__/Header.spec.tsx +2 -2
- package/src/components/tables/cells/cell.id.tsx +13 -2
- package/src/core/abstracts/AbstractService.ts +3 -3
- package/src/core/abstracts/ClientAbstractService.ts +3 -2
- package/src/features/assistant-action/components/ApprovalActionCard.tsx +74 -5
- package/src/features/assistant-action/components/__tests__/ApprovalActionCard.spec.tsx +81 -0
- package/src/features/assistant-message/components/parts/RelevanceMeter.tsx +4 -1
- package/src/features/assistant-message/components/parts/tabs/UsersTab.tsx +2 -1
- package/src/features/auth/components/buttons/GoogleSignInButton.tsx +4 -1
- package/src/features/auth/components/details/LandingComponent.tsx +1 -1
- package/src/features/auth/components/forms/ForgotPassword.tsx +1 -1
- package/src/features/auth/components/forms/Login.tsx +1 -1
- package/src/features/auth/components/forms/Register.tsx +1 -1
- package/src/features/auth/components/forms/ResetPassword.tsx +2 -2
- package/src/features/auth/components/two-factor/BackupCodesDialog.tsx +1 -1
- package/src/features/auth/components/two-factor/PasskeyButton.tsx +1 -1
- package/src/features/billing/components/cards/BillingUsageSummaryCard.tsx +21 -11
- package/src/features/billing/components/cards/CustomerInfoCard.tsx +16 -13
- package/src/features/billing/components/cards/InvoicesSummaryCard.tsx +33 -21
- package/src/features/billing/components/cards/PaymentMethodSummaryCard.tsx +20 -10
- package/src/features/billing/components/cards/SubscriptionSummaryCard.tsx +35 -22
- package/src/features/billing/components/containers/BillingDashboardContainer.tsx +24 -24
- package/src/features/billing/components/utils/__tests__/billing-formatters.spec.ts +63 -0
- package/src/features/billing/components/utils/currency.ts +67 -10
- package/src/features/billing/components/utils/date.ts +9 -4
- package/src/features/billing/components/utils/format-options.ts +20 -0
- package/src/features/billing/components/utils/index.ts +3 -1
- package/src/features/billing/components/utils/useBillingFormatOptions.ts +11 -0
- package/src/features/billing/components/widgets/BillingAlertBanner.tsx +8 -9
- package/src/features/billing/contexts/PriceContext.tsx +18 -6
- package/src/features/billing/i18n-keys.ts +212 -0
- package/src/features/billing/stripe-customer/components/containers/PaymentMethodsContainer.tsx +8 -8
- package/src/features/billing/stripe-customer/components/details/PaymentMethodCard.tsx +11 -9
- package/src/features/billing/stripe-customer/components/forms/PaymentMethodEditor.tsx +4 -4
- package/src/features/billing/stripe-customer/components/forms/PaymentMethodForm.tsx +9 -7
- package/src/features/billing/stripe-invoice/components/containers/InvoicesContainer.tsx +10 -8
- package/src/features/billing/stripe-invoice/components/details/InvoiceDetails.tsx +34 -29
- package/src/features/billing/stripe-invoice/components/lists/InvoicesList.tsx +12 -9
- package/src/features/billing/stripe-invoice/components/widgets/InvoiceStatusBadge.tsx +9 -6
- package/src/features/billing/stripe-price/components/details/PriceDetails.tsx +4 -3
- package/src/features/billing/stripe-price/components/forms/PriceArchiver.tsx +3 -1
- package/src/features/billing/stripe-price/components/forms/PriceEditor.tsx +3 -1
- package/src/features/billing/stripe-price/hooks/useStripePriceTableStructure.tsx +6 -5
- package/src/features/billing/stripe-promotion-code/components/PromoCodeInput.tsx +12 -7
- package/src/features/billing/stripe-promotion-code/data/stripe-promotion-code.service.ts +3 -1
- package/src/features/billing/stripe-subscription/components/containers/SubscriptionsContainer.tsx +10 -6
- package/src/features/billing/stripe-subscription/components/details/SubscriptionDetails.tsx +44 -29
- package/src/features/billing/stripe-subscription/components/forms/CancelSubscriptionDialog.tsx +14 -11
- package/src/features/billing/stripe-subscription/components/lists/SubscriptionsList.tsx +20 -17
- package/src/features/billing/stripe-subscription/components/widgets/IntervalToggle.tsx +4 -2
- package/src/features/billing/stripe-subscription/components/widgets/PricingCard.tsx +16 -7
- package/src/features/billing/stripe-subscription/components/widgets/ProductPricingList.tsx +3 -1
- package/src/features/billing/stripe-subscription/components/widgets/ProductPricingRow.tsx +3 -1
- package/src/features/billing/stripe-subscription/components/widgets/ProrationPreview.tsx +12 -7
- package/src/features/billing/stripe-subscription/components/widgets/SubscriptionConfirmation.tsx +11 -8
- package/src/features/billing/stripe-subscription/components/widgets/SubscriptionStatusBadge.tsx +13 -10
- package/src/features/billing/stripe-subscription/components/wizards/SubscriptionWizard.tsx +8 -6
- package/src/features/billing/stripe-subscription/components/wizards/WizardProgressIndicator.tsx +7 -4
- package/src/features/billing/stripe-subscription/components/wizards/WizardStepPaymentMethod.tsx +4 -2
- package/src/features/billing/stripe-subscription/components/wizards/WizardStepPlanSelection.tsx +3 -1
- package/src/features/billing/stripe-subscription/components/wizards/WizardStepReview.tsx +37 -28
- package/src/features/billing/stripe-subscription/hooks/useConfirmSubscriptionPayment.ts +9 -7
- package/src/features/billing/stripe-subscription/hooks/useSubscriptionWizard.ts +20 -9
- package/src/features/billing/stripe-usage/components/containers/UsageContainer.tsx +6 -6
- package/src/features/billing/stripe-usage/components/details/UsageSummaryCard.tsx +24 -9
- package/src/features/billing/stripe-usage/components/lists/UsageHistoryTable.tsx +16 -12
- package/src/features/company/components/details/TokenStatusIndicator.tsx +7 -5
- package/src/features/company/components/forms/CompanyEditor.tsx +2 -2
- package/src/features/help/components/HelpArticleBody.tsx +1 -1
- package/src/features/help/components/HelpHeader.tsx +1 -1
- package/src/features/help/components/HelpSideNav.tsx +1 -1
- package/src/features/help/i18n-keys.ts +2 -0
- package/src/features/notification/components/common/NotificationErrorBoundary.tsx +17 -12
- package/src/features/notification/components/containers/NotificationsListContainer.tsx +4 -2
- package/src/features/notification/components/notifications/Notification.tsx +6 -3
- package/src/features/notification/contexts/NotificationContext.tsx +9 -4
- package/src/features/notification/contexts/__tests__/NotificationContext.spec.tsx +13 -0
- package/src/features/notification/hooks/useNotificationTableStructure.tsx +4 -2
- package/src/features/oauth/components/OAuthClientCard.tsx +13 -7
- package/src/features/oauth/components/OAuthClientDetail.tsx +34 -30
- package/src/features/oauth/components/OAuthClientForm.tsx +24 -24
- package/src/features/oauth/components/OAuthClientList.tsx +10 -6
- package/src/features/oauth/components/OAuthClientSecretDisplay.tsx +10 -8
- package/src/features/oauth/components/OAuthRedirectUriInput.tsx +9 -13
- package/src/features/oauth/components/OAuthScopeSelector.tsx +15 -6
- package/src/features/oauth/components/__tests__/OAuthClientForm.spec.tsx +11 -9
- package/src/features/oauth/components/consent/OAuthScopeList.tsx +4 -2
- package/src/features/oauth/hooks/__tests__/useOAuthScopeText.spec.ts +30 -0
- package/src/features/oauth/hooks/index.ts +1 -0
- package/src/features/oauth/hooks/useOAuthClient.ts +11 -5
- package/src/features/oauth/hooks/useOAuthClients.ts +9 -3
- package/src/features/oauth/hooks/useOAuthConsent.ts +11 -5
- package/src/features/oauth/hooks/useOAuthScopeText.ts +33 -0
- package/src/features/rbac/components/RbacByRoleContainer.tsx +7 -6
- package/src/features/rbac/components/RbacContainer.tsx +7 -6
- package/src/features/rbac/contexts/RbacContext.tsx +3 -3
- package/src/features/user/components/containers/UserContainer.tsx +3 -1
- package/src/features/user/data/__tests__/user.test.ts +66 -0
- package/src/features/user/data/user.interface.ts +2 -0
- package/src/features/user/data/user.service.ts +9 -0
- package/src/features/user/data/user.ts +7 -0
- package/src/features/waitlist/components/forms/WaitlistForm.tsx +1 -1
- package/src/features/waitlist/components/lists/WaitlistList.tsx +1 -1
- package/src/i18n/__tests__/config.test.ts +21 -1
- package/src/i18n/config.ts +13 -0
- package/src/i18n/index.ts +1 -0
- package/src/shadcnui/custom/multi-select.tsx +47 -14
- package/src/shadcnui/custom/multiple-selector.tsx +14 -2
- package/src/shadcnui/ui/__tests__/use-ui-text.test.tsx +76 -0
- package/src/shadcnui/ui/breadcrumb.tsx +9 -2
- package/src/shadcnui/ui/carousel.tsx +15 -4
- package/src/shadcnui/ui/dialog.tsx +15 -2
- package/src/shadcnui/ui/sheet.tsx +6 -1
- package/src/shadcnui/ui/sidebar.tsx +33 -7
- package/src/shadcnui/ui/spinner.tsx +6 -1
- package/src/shadcnui/ui/use-ui-text.ts +49 -0
- package/src/utils/__tests__/date-formatter.test.ts +24 -1
- package/src/utils/date-formatter.ts +16 -0
- package/src/utils/index.ts +1 -1
- package/dist/BlockNoteEditor-BENYTTCZ.js.map +0 -1
- package/dist/BlockNoteEditor-VOYKZOP5.mjs.map +0 -1
- package/dist/chunk-4BZJ4LAB.js.map +0 -1
- package/dist/chunk-6ISF5KNQ.mjs.map +0 -1
- package/dist/chunk-ORU7KLCY.js.map +0 -1
- package/dist/chunk-ZZ6E33FO.mjs.map +0 -1
- /package/dist/{BlockNoteViewer-Y2ZM4ESA.mjs.map → BlockNoteViewer-MDPO6BXN.mjs.map} +0 -0
- /package/dist/{chunk-46S3KPLU.mjs.map → chunk-ILFFC6BG.mjs.map} +0 -0
- /package/dist/{chunk-HZVVRIJQ.mjs.map → chunk-IUS2RHCX.mjs.map} +0 -0
- /package/dist/{chunk-ZO3QQ5CQ.mjs.map → chunk-MQDR6MEE.mjs.map} +0 -0
|
@@ -108,6 +108,69 @@ function humaniseKey(key: string): string {
|
|
|
108
108
|
return words.charAt(0).toUpperCase() + words.slice(1);
|
|
109
109
|
}
|
|
110
110
|
|
|
111
|
+
/**
|
|
112
|
+
* The untyped call signature of next-intl's `t`. Every key below is computed
|
|
113
|
+
* from wire identifiers, so the generated message-key union cannot describe it.
|
|
114
|
+
*/
|
|
115
|
+
type LooseTranslate = ((key: string, values?: Record<string, unknown>) => string) & {
|
|
116
|
+
has?: (key: string) => boolean;
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Translates `key` when the host app defines it, else `undefined`. `t.has` is
|
|
121
|
+
* the only non-throwing probe: a plain `t()` on a missing key either throws or
|
|
122
|
+
* returns the key path, depending on the app's next-intl error handler.
|
|
123
|
+
*/
|
|
124
|
+
function translateIfPresent(t: LooseTranslate, key: string, values?: Record<string, unknown>): string | undefined {
|
|
125
|
+
if (typeof t.has !== "function" || !t.has(key)) return undefined;
|
|
126
|
+
try {
|
|
127
|
+
const text = t(key, values);
|
|
128
|
+
return text && text !== key ? text : undefined;
|
|
129
|
+
} catch {
|
|
130
|
+
return undefined;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Message-key segments a JSON:API type may be filed under in the app's
|
|
136
|
+
* `features.<feature>` namespace: `npcs` → `npc`, `knives` → `knife`,
|
|
137
|
+
* `stories` → `story`, `scene-cornerstones` → `scene-cornerstone` and
|
|
138
|
+
* `sceneCornerstone`.
|
|
139
|
+
*/
|
|
140
|
+
function featureKeyCandidates(type: string): string[] {
|
|
141
|
+
if (!type) return [];
|
|
142
|
+
const singulars = new Set<string>();
|
|
143
|
+
if (type.endsWith("ives")) singulars.add(`${type.slice(0, -4)}ife`);
|
|
144
|
+
if (type.endsWith("ies")) singulars.add(`${type.slice(0, -3)}y`);
|
|
145
|
+
if (type.endsWith("s")) singulars.add(type.slice(0, -1));
|
|
146
|
+
singulars.add(type);
|
|
147
|
+
const candidates = new Set<string>();
|
|
148
|
+
for (const singular of singulars) {
|
|
149
|
+
candidates.add(singular);
|
|
150
|
+
candidates.add(singular.replace(/[-_]+([a-z0-9])/g, (_, letter: string) => letter.toUpperCase()));
|
|
151
|
+
}
|
|
152
|
+
return [...candidates];
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Label of an attribute or relationship, in the active language. Tries, in
|
|
157
|
+
* order: the entity editor's own label (`features.<feature>.fields.<key>.label`),
|
|
158
|
+
* the generic `ui.fields.<key>`, then the humanised key — so a field no app
|
|
159
|
+
* catalogue knows still renders exactly as it did before translation existed.
|
|
160
|
+
*/
|
|
161
|
+
function fieldLabel(t: LooseTranslate, type: string, key: string): string {
|
|
162
|
+
for (const feature of featureKeyCandidates(type)) {
|
|
163
|
+
const label = translateIfPresent(t, `features.${feature}.fields.${key}.label`);
|
|
164
|
+
if (label !== undefined) return label;
|
|
165
|
+
}
|
|
166
|
+
return translateIfPresent(t, `ui.fields.${key}`) ?? humaniseKey(key);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** Singular entity name from the app's `entities.<type>` plural message, else the humanised type. */
|
|
170
|
+
function entityName(t: LooseTranslate, type: string): string {
|
|
171
|
+
return translateIfPresent(t, `entities.${type}`, { count: 1 }) ?? humaniseType(type);
|
|
172
|
+
}
|
|
173
|
+
|
|
111
174
|
/**
|
|
112
175
|
* Plain text of a BlockNote document, without importing BlockNote: walk the
|
|
113
176
|
* blocks, concatenate the text runs of each block's `content`, recurse into
|
|
@@ -336,7 +399,9 @@ export function ApprovalActionCard({ actionId, summary, onResolved, hiddenRelati
|
|
|
336
399
|
const presented = useMemo(() => parseOperatorProposal(actionProposal), [actionProposal]);
|
|
337
400
|
const legacy = useMemo(() => (presented ? undefined : parseLegacyProposal(toolArgs)), [presented, toolArgs]);
|
|
338
401
|
const isUpdate = toolName === "update_entity";
|
|
339
|
-
const
|
|
402
|
+
const entityType = presented?.type ?? legacy?.type ?? "";
|
|
403
|
+
const loose = t as unknown as LooseTranslate;
|
|
404
|
+
const entityLabel = entityName(loose, entityType);
|
|
340
405
|
const targetLabel = presented?.target?.label ?? "";
|
|
341
406
|
|
|
342
407
|
const rows = useMemo<ProposalRow[]>(() => {
|
|
@@ -345,7 +410,9 @@ export function ApprovalActionCard({ actionId, summary, onResolved, hiddenRelati
|
|
|
345
410
|
const attributes = presented?.attributes ?? legacy?.attributes;
|
|
346
411
|
const attributeRows = Object.entries(attributes ?? {}).flatMap<ProposalRow>(([key, value]) => {
|
|
347
412
|
const text = formatAttribute(value, yes, no);
|
|
348
|
-
return text === undefined
|
|
413
|
+
return text === undefined
|
|
414
|
+
? []
|
|
415
|
+
: [{ key: `attribute:${key}`, label: fieldLabel(loose, entityType, key), value: text }];
|
|
349
416
|
});
|
|
350
417
|
if (!presented) return attributeRows;
|
|
351
418
|
|
|
@@ -353,7 +420,9 @@ export function ApprovalActionCard({ actionId, summary, onResolved, hiddenRelati
|
|
|
353
420
|
const relationshipRows = Object.entries(presented.relationships ?? {}).flatMap<ProposalRow>(([key, refs]) => {
|
|
354
421
|
if (hidden.has(key)) return [];
|
|
355
422
|
const text = formatRefs(refs);
|
|
356
|
-
return text === undefined
|
|
423
|
+
return text === undefined
|
|
424
|
+
? []
|
|
425
|
+
: [{ key: `relationship:${key}`, label: fieldLabel(loose, entityType, key), value: text }];
|
|
357
426
|
});
|
|
358
427
|
|
|
359
428
|
const linkRows: ProposalRow[] = [];
|
|
@@ -361,7 +430,7 @@ export function ApprovalActionCard({ actionId, summary, onResolved, hiddenRelati
|
|
|
361
430
|
linkRows.push({
|
|
362
431
|
key: "link:relationship",
|
|
363
432
|
label: t("features.assistant.approval.proposal.relationship"),
|
|
364
|
-
value:
|
|
433
|
+
value: fieldLabel(loose, entityType, presented.relationship),
|
|
365
434
|
});
|
|
366
435
|
}
|
|
367
436
|
const targetsText = formatRefs(presented.targets ?? []);
|
|
@@ -374,7 +443,7 @@ export function ApprovalActionCard({ actionId, summary, onResolved, hiddenRelati
|
|
|
374
443
|
}
|
|
375
444
|
|
|
376
445
|
return [...attributeRows, ...relationshipRows, ...linkRows];
|
|
377
|
-
}, [presented, legacy, t, hiddenRelationshipKeys]);
|
|
446
|
+
}, [presented, legacy, t, loose, entityType, hiddenRelationshipKeys]);
|
|
378
447
|
|
|
379
448
|
const hasProposal = rows.length > 0;
|
|
380
449
|
|
|
@@ -2,6 +2,25 @@ import { render, screen } from "@testing-library/react";
|
|
|
2
2
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
3
3
|
import { ApprovalActionCard } from "../ApprovalActionCard";
|
|
4
4
|
|
|
5
|
+
/**
|
|
6
|
+
* A message catalogue the test fills in. Keys it does not hold behave like a
|
|
7
|
+
* host app without them: `t()` hands back the key path and `t.has()` is false,
|
|
8
|
+
* so the card must fall back to the humanised attribute name.
|
|
9
|
+
*/
|
|
10
|
+
const messages = vi.hoisted(() => ({}) as Record<string, string>);
|
|
11
|
+
|
|
12
|
+
vi.mock("next-intl", () => {
|
|
13
|
+
const t = (key: string) => messages[key] ?? key;
|
|
14
|
+
(t as unknown as { has: (key: string) => boolean }).has = (key: string) => key in messages;
|
|
15
|
+
return {
|
|
16
|
+
useTranslations: () => t,
|
|
17
|
+
useLocale: () => "en",
|
|
18
|
+
useNow: () => new Date(),
|
|
19
|
+
useTimeZone: () => "UTC",
|
|
20
|
+
useFormatter: () => ({ dateTime: vi.fn(), number: vi.fn(), relativeTime: vi.fn() }),
|
|
21
|
+
};
|
|
22
|
+
});
|
|
23
|
+
|
|
5
24
|
const findOne = vi.fn();
|
|
6
25
|
const approve = vi.fn();
|
|
7
26
|
const deny = vi.fn();
|
|
@@ -45,6 +64,7 @@ function makeAction(overrides: Record<string, unknown> = {}) {
|
|
|
45
64
|
|
|
46
65
|
describe("ApprovalActionCard", () => {
|
|
47
66
|
beforeEach(() => {
|
|
67
|
+
for (const key of Object.keys(messages)) delete messages[key];
|
|
48
68
|
findOne.mockReset();
|
|
49
69
|
approve.mockReset();
|
|
50
70
|
deny.mockReset();
|
|
@@ -254,6 +274,67 @@ describe("ApprovalActionCard", () => {
|
|
|
254
274
|
expect(screen.queryByText(UUID_LIKE)).toBeNull();
|
|
255
275
|
});
|
|
256
276
|
|
|
277
|
+
it("labels fields with the entity editor's own message keys when the app defines them", async () => {
|
|
278
|
+
Object.assign(messages, {
|
|
279
|
+
"features.npc.fields.name.label": "Nome",
|
|
280
|
+
"features.npc.fields.tldr.label": "Riassunto",
|
|
281
|
+
"entities.npcs": "PNG",
|
|
282
|
+
});
|
|
283
|
+
findOne.mockResolvedValue(makeAction());
|
|
284
|
+
|
|
285
|
+
render(<ApprovalActionCard actionId="act-1" />);
|
|
286
|
+
|
|
287
|
+
expect(await screen.findByText("Nome")).toBeInTheDocument();
|
|
288
|
+
expect(screen.getByText("Riassunto")).toBeInTheDocument();
|
|
289
|
+
// `related` has no editor key and no generic key: humanised, as before
|
|
290
|
+
expect(screen.getByText("Related")).toBeInTheDocument();
|
|
291
|
+
expect(screen.queryByText("Name")).toBeNull();
|
|
292
|
+
expect(screen.queryByText("Tldr")).toBeNull();
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
it("falls back to the generic ui.fields key, and finds editor keys for irregular plurals", async () => {
|
|
296
|
+
Object.assign(messages, {
|
|
297
|
+
"ui.fields.related": "Collegati",
|
|
298
|
+
"features.knife.fields.name.label": "Nome del gancio",
|
|
299
|
+
});
|
|
300
|
+
findOne.mockResolvedValue(
|
|
301
|
+
makeAction({
|
|
302
|
+
proposal: JSON.stringify({
|
|
303
|
+
type: "knives",
|
|
304
|
+
attributes: { name: "The sister left behind", publicDescription: "Visible" },
|
|
305
|
+
relationships: { related: [{ id: "r1", type: "npcs", label: "Zoe" }] },
|
|
306
|
+
}),
|
|
307
|
+
}),
|
|
308
|
+
);
|
|
309
|
+
|
|
310
|
+
render(<ApprovalActionCard actionId="act-1" />);
|
|
311
|
+
|
|
312
|
+
expect(await screen.findByText("Nome del gancio")).toBeInTheDocument();
|
|
313
|
+
expect(screen.getByText("Collegati")).toBeInTheDocument();
|
|
314
|
+
// no key anywhere: the humanised attribute name, unchanged
|
|
315
|
+
expect(screen.getByText("Public Description")).toBeInTheDocument();
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
it("translates the relationship name on a link proposal", async () => {
|
|
319
|
+
Object.assign(messages, { "features.clue.fields.containedInScene.label": "Contenuto nella scena" });
|
|
320
|
+
findOne.mockResolvedValue(
|
|
321
|
+
makeAction({
|
|
322
|
+
toolName: "link_entities",
|
|
323
|
+
proposal: JSON.stringify({
|
|
324
|
+
type: "clues",
|
|
325
|
+
target: { id: "c1", type: "clues", label: "Bloody knife" },
|
|
326
|
+
relationship: "containedInScene",
|
|
327
|
+
targets: [{ id: "s1", type: "scenes", label: "The docks" }],
|
|
328
|
+
}),
|
|
329
|
+
}),
|
|
330
|
+
);
|
|
331
|
+
|
|
332
|
+
render(<ApprovalActionCard actionId="act-1" />);
|
|
333
|
+
|
|
334
|
+
expect(await screen.findByText("Contenuto nella scena")).toBeInTheDocument();
|
|
335
|
+
expect(screen.getByText("The docks")).toBeInTheDocument();
|
|
336
|
+
});
|
|
337
|
+
|
|
257
338
|
it("still renders the summary and the buttons when neither payload parses", async () => {
|
|
258
339
|
findOne.mockResolvedValue(makeAction({ proposal: "{not-json", toolArgs: "{not-json" }));
|
|
259
340
|
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
+
import { useTranslations } from "next-intl";
|
|
4
|
+
|
|
3
5
|
interface Props {
|
|
4
6
|
/** 0-1 (decimal) or 0-100 (percent). Auto-detected. */
|
|
5
7
|
value: number;
|
|
@@ -7,6 +9,7 @@ interface Props {
|
|
|
7
9
|
}
|
|
8
10
|
|
|
9
11
|
export function RelevanceMeter({ value, className = "" }: Props) {
|
|
12
|
+
const t = useTranslations();
|
|
10
13
|
const pct = Math.max(0, Math.min(100, value <= 1 ? value * 100 : value));
|
|
11
14
|
const label = `${Math.round(pct)}%`;
|
|
12
15
|
return (
|
|
@@ -15,7 +18,7 @@ export function RelevanceMeter({ value, className = "" }: Props) {
|
|
|
15
18
|
aria-valuenow={Math.round(pct)}
|
|
16
19
|
aria-valuemin={0}
|
|
17
20
|
aria-valuemax={100}
|
|
18
|
-
aria-label={
|
|
21
|
+
aria-label={`${t("common.relevance")} ${label}`}
|
|
19
22
|
className={`bg-muted relative mx-auto flex h-5 w-20 items-center justify-center overflow-hidden rounded border ${className}`}
|
|
20
23
|
>
|
|
21
24
|
<div className="bg-accent absolute top-0 start-0 h-full" style={{ width: `${pct}%` }} />
|
|
@@ -101,7 +101,8 @@ export function UsersTab({ users, citations, sources }: Props) {
|
|
|
101
101
|
return null;
|
|
102
102
|
}
|
|
103
103
|
const href = generate({ page: module, id: user.id });
|
|
104
|
-
const name =
|
|
104
|
+
const name =
|
|
105
|
+
(user as any).name ?? (user as any).fullName ?? user.identifier ?? t("entities.users", { count: 1 });
|
|
105
106
|
const avatarUrl = (user as any).avatar as string | undefined;
|
|
106
107
|
const showCounts = citationCount > 0 || contentCount > 0;
|
|
107
108
|
return (
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
+
import { useTranslations } from "next-intl";
|
|
3
4
|
import { getPublicApiUrl } from "../../../../client/config";
|
|
4
5
|
import { isGoogleAuthEnabled } from "../../../../login";
|
|
5
6
|
import { Button, Link } from "../../../../shadcnui";
|
|
@@ -9,6 +10,8 @@ interface GoogleSignInButtonProps {
|
|
|
9
10
|
}
|
|
10
11
|
|
|
11
12
|
export function GoogleSignInButton({ referralCode }: GoogleSignInButtonProps) {
|
|
13
|
+
const t = useTranslations();
|
|
14
|
+
|
|
12
15
|
if (!isGoogleAuthEnabled()) {
|
|
13
16
|
return null;
|
|
14
17
|
}
|
|
@@ -45,7 +48,7 @@ export function GoogleSignInButton({ referralCode }: GoogleSignInButtonProps) {
|
|
|
45
48
|
d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"
|
|
46
49
|
/>
|
|
47
50
|
</svg>
|
|
48
|
-
|
|
51
|
+
{t("auth.buttons.sign_in_with_google")}
|
|
49
52
|
</Button>
|
|
50
53
|
</Link>
|
|
51
54
|
);
|
|
@@ -55,7 +55,7 @@ export function LandingComponent() {
|
|
|
55
55
|
{isDiscordAuthEnabled() && (
|
|
56
56
|
<Link href={`${getPublicApiUrl()}auth/discord`} className="flex w-full justify-end">
|
|
57
57
|
<Button className="w-full" variant={`outline`} data-testid="page-login-button-initial-login">
|
|
58
|
-
|
|
58
|
+
{t(`auth.buttons.login_with_discord`)}
|
|
59
59
|
</Button>
|
|
60
60
|
</Link>
|
|
61
61
|
)}
|
|
@@ -152,7 +152,7 @@ export function Login() {
|
|
|
152
152
|
{isDiscordAuthEnabled() && (
|
|
153
153
|
<Link href={buildDiscordOAuthUrl()} className="flex w-full justify-end">
|
|
154
154
|
<Button className="w-full" variant={`outline`} data-testid="page-login-button-initial-login">
|
|
155
|
-
|
|
155
|
+
{t(`auth.buttons.login_with_discord`)}
|
|
156
156
|
</Button>
|
|
157
157
|
</Link>
|
|
158
158
|
)}
|
|
@@ -106,8 +106,8 @@ export function ResetPassword() {
|
|
|
106
106
|
) : (
|
|
107
107
|
<Form {...form}>
|
|
108
108
|
<form onSubmit={form.handleSubmit(onSubmit)}>
|
|
109
|
-
<FormPassword form={form} id="password" name={
|
|
110
|
-
<FormPassword form={form} id="passwordRetype" name={
|
|
109
|
+
<FormPassword form={form} id="password" name={t(`user.fields.password.label`)} />
|
|
110
|
+
<FormPassword form={form} id="passwordRetype" name={t(`auth.fields.retype_password.retype_label`)} />
|
|
111
111
|
<Button className="mt-4 w-full" type={"submit"}>
|
|
112
112
|
{t(`auth.buttons.reset_password`)}
|
|
113
113
|
</Button>
|
|
@@ -59,7 +59,7 @@ export function BackupCodesDialog({ remainingCodes, onRegenerate, trigger }: Bac
|
|
|
59
59
|
};
|
|
60
60
|
|
|
61
61
|
const handleDownload = () => {
|
|
62
|
-
const text =
|
|
62
|
+
const text = `${t("auth.two_factor.backup_codes")}\n\n${codes.join("\n")}\n\n${t("auth.two_factor.backup_codes_file_note")}`;
|
|
63
63
|
const blob = new Blob([text], { type: "text/plain" });
|
|
64
64
|
const url = URL.createObjectURL(blob);
|
|
65
65
|
const a = document.createElement("a");
|
|
@@ -38,7 +38,7 @@ export function PasskeyButton({ pendingToken, onSuccess, onError, disabled = fal
|
|
|
38
38
|
|
|
39
39
|
onSuccess(auth);
|
|
40
40
|
} catch (error) {
|
|
41
|
-
onError(error instanceof Error ? error : new Error("
|
|
41
|
+
onError(error instanceof Error ? error : new Error(t("auth.two_factor.passkey_failed")));
|
|
42
42
|
} finally {
|
|
43
43
|
setIsLoading(false);
|
|
44
44
|
}
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
+
import { useTranslations } from "next-intl";
|
|
3
4
|
import { Activity, ChevronRight } from "lucide-react";
|
|
4
5
|
import { Card, CardContent, CardHeader, CardTitle, Skeleton } from "../../../../shadcnui";
|
|
6
|
+
import { useI18nLocale } from "../../../../i18n";
|
|
5
7
|
import { MeterInterface, MeterSummaryInterface } from "../../stripe-usage";
|
|
6
8
|
|
|
7
9
|
type BillingUsageSummaryCardProps = {
|
|
@@ -12,8 +14,8 @@ type BillingUsageSummaryCardProps = {
|
|
|
12
14
|
onViewDetailsClick: () => void;
|
|
13
15
|
};
|
|
14
16
|
|
|
15
|
-
function formatNumber(value: number): string {
|
|
16
|
-
return new Intl.NumberFormat(
|
|
17
|
+
function formatNumber(value: number, locale: string): string {
|
|
18
|
+
return new Intl.NumberFormat(locale, {
|
|
17
19
|
notation: "compact",
|
|
18
20
|
compactDisplay: "short",
|
|
19
21
|
}).format(value);
|
|
@@ -26,11 +28,13 @@ export function BillingUsageSummaryCard({
|
|
|
26
28
|
error,
|
|
27
29
|
onViewDetailsClick,
|
|
28
30
|
}: BillingUsageSummaryCardProps) {
|
|
31
|
+
const t = useTranslations();
|
|
32
|
+
const locale = useI18nLocale();
|
|
29
33
|
if (loading) {
|
|
30
34
|
return (
|
|
31
35
|
<Card>
|
|
32
36
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
|
33
|
-
<CardTitle>
|
|
37
|
+
<CardTitle>{t("billing.ui.cards.usage.title")}</CardTitle>
|
|
34
38
|
<Activity className="h-4 w-4 text-muted-foreground" />
|
|
35
39
|
</CardHeader>
|
|
36
40
|
<CardContent>
|
|
@@ -45,7 +49,7 @@ export function BillingUsageSummaryCard({
|
|
|
45
49
|
return (
|
|
46
50
|
<Card>
|
|
47
51
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
|
48
|
-
<CardTitle>
|
|
52
|
+
<CardTitle>{t("billing.ui.cards.usage.title")}</CardTitle>
|
|
49
53
|
<Activity className="h-4 w-4 text-muted-foreground" />
|
|
50
54
|
</CardHeader>
|
|
51
55
|
<CardContent>
|
|
@@ -67,26 +71,32 @@ export function BillingUsageSummaryCard({
|
|
|
67
71
|
return (
|
|
68
72
|
<Card className="cursor-pointer hover:bg-accent/50 transition-colors" onClick={onViewDetailsClick}>
|
|
69
73
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
|
70
|
-
<CardTitle>
|
|
74
|
+
<CardTitle>{t("billing.ui.cards.usage.title")}</CardTitle>
|
|
71
75
|
<Activity className="h-4 w-4 text-muted-foreground" />
|
|
72
76
|
</CardHeader>
|
|
73
77
|
<CardContent>
|
|
74
78
|
{meters.length === 0 ? (
|
|
75
79
|
<div className="space-y-2">
|
|
76
|
-
<p className="text-xl font-bold text-muted-foreground">
|
|
77
|
-
<p className="text-xs text-muted-foreground">
|
|
80
|
+
<p className="text-xl font-bold text-muted-foreground">{t("billing.ui.cards.usage.no_meters")}</p>
|
|
81
|
+
<p className="text-xs text-muted-foreground">{t("billing.ui.cards.usage.no_meters_description")}</p>
|
|
78
82
|
</div>
|
|
79
83
|
) : (
|
|
80
84
|
<div className="space-y-2">
|
|
81
|
-
<p className="text-xl font-bold">
|
|
85
|
+
<p className="text-xl font-bold">
|
|
86
|
+
{t("billing.ui.cards.usage.units", { value: formatNumber(totalUsage, locale) })}
|
|
87
|
+
</p>
|
|
82
88
|
{primaryMeter && primarySummary && (
|
|
83
89
|
<p className="text-sm text-muted-foreground">
|
|
84
|
-
{primaryMeter.displayName}: {formatNumber(primarySummary.aggregatedValue)}
|
|
90
|
+
{primaryMeter.displayName}: {formatNumber(primarySummary.aggregatedValue, locale)}
|
|
91
|
+
</p>
|
|
92
|
+
)}
|
|
93
|
+
{meters.length > 1 && (
|
|
94
|
+
<p className="text-xs text-muted-foreground">
|
|
95
|
+
{t("billing.ui.cards.usage.across_meters", { count: meters.length })}
|
|
85
96
|
</p>
|
|
86
97
|
)}
|
|
87
|
-
{meters.length > 1 && <p className="text-xs text-muted-foreground">Across {meters.length} meters</p>}
|
|
88
98
|
<span className="flex items-center text-xs text-muted-foreground">
|
|
89
|
-
|
|
99
|
+
{t("billing.ui.cards.view_details")}
|
|
90
100
|
<ChevronRight className="h-3 w-3 ms-1 rtl:rotate-180" />
|
|
91
101
|
</span>
|
|
92
102
|
</div>
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
+
import { useTranslations } from "next-intl";
|
|
3
4
|
import { ExternalLink, User } from "lucide-react";
|
|
4
5
|
import { useState } from "react";
|
|
5
6
|
import { Button, Card, CardContent, CardHeader, CardTitle, Skeleton } from "../../../../shadcnui";
|
|
7
|
+
import { useI18nLocale } from "../../../../i18n";
|
|
6
8
|
import { StripeCustomerInterface, StripeCustomerService } from "../../stripe-customer";
|
|
7
9
|
|
|
8
10
|
type CustomerInfoCardProps = {
|
|
@@ -11,18 +13,19 @@ type CustomerInfoCardProps = {
|
|
|
11
13
|
error?: string;
|
|
12
14
|
};
|
|
13
15
|
|
|
14
|
-
function formatBalance(balance: number | undefined, currency: string | undefined): string {
|
|
15
|
-
if (balance === undefined || balance === 0) return "$0.00";
|
|
16
|
+
function formatBalance(balance: number | undefined, currency: string | undefined, locale: string): string {
|
|
16
17
|
const currencyCode = currency?.toUpperCase() || "USD";
|
|
17
18
|
// Balance in Stripe is negative when customer has credit
|
|
18
|
-
const displayBalance = -balance;
|
|
19
|
-
return new Intl.NumberFormat(
|
|
19
|
+
const displayBalance = balance ? -balance : 0;
|
|
20
|
+
return new Intl.NumberFormat(locale, {
|
|
20
21
|
style: "currency",
|
|
21
22
|
currency: currencyCode,
|
|
22
23
|
}).format(displayBalance / 100);
|
|
23
24
|
}
|
|
24
25
|
|
|
25
26
|
export function CustomerInfoCard({ customer, loading, error }: CustomerInfoCardProps) {
|
|
27
|
+
const t = useTranslations();
|
|
28
|
+
const locale = useI18nLocale();
|
|
26
29
|
const [portalLoading, setPortalLoading] = useState(false);
|
|
27
30
|
|
|
28
31
|
const handlePortalClick = async (e: React.MouseEvent) => {
|
|
@@ -42,7 +45,7 @@ export function CustomerInfoCard({ customer, loading, error }: CustomerInfoCardP
|
|
|
42
45
|
return (
|
|
43
46
|
<Card>
|
|
44
47
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
|
45
|
-
<CardTitle>
|
|
48
|
+
<CardTitle>{t("billing.ui.cards.customer.title")}</CardTitle>
|
|
46
49
|
<User className="h-4 w-4 text-muted-foreground" />
|
|
47
50
|
</CardHeader>
|
|
48
51
|
<CardContent>
|
|
@@ -58,7 +61,7 @@ export function CustomerInfoCard({ customer, loading, error }: CustomerInfoCardP
|
|
|
58
61
|
return (
|
|
59
62
|
<Card>
|
|
60
63
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
|
61
|
-
<CardTitle>
|
|
64
|
+
<CardTitle>{t("billing.ui.cards.customer.title")}</CardTitle>
|
|
62
65
|
<User className="h-4 w-4 text-muted-foreground" />
|
|
63
66
|
</CardHeader>
|
|
64
67
|
<CardContent>
|
|
@@ -72,12 +75,12 @@ export function CustomerInfoCard({ customer, loading, error }: CustomerInfoCardP
|
|
|
72
75
|
return (
|
|
73
76
|
<Card>
|
|
74
77
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
|
75
|
-
<CardTitle>
|
|
78
|
+
<CardTitle>{t("billing.ui.cards.customer.title")}</CardTitle>
|
|
76
79
|
<User className="h-4 w-4 text-muted-foreground" />
|
|
77
80
|
</CardHeader>
|
|
78
81
|
<CardContent>
|
|
79
|
-
<p className="text-xl font-bold text-muted-foreground">
|
|
80
|
-
<p className="text-xs text-muted-foreground">
|
|
82
|
+
<p className="text-xl font-bold text-muted-foreground">{t("billing.ui.cards.customer.not_set_up")}</p>
|
|
83
|
+
<p className="text-xs text-muted-foreground">{t("billing.ui.cards.customer.not_set_up_description")}</p>
|
|
81
84
|
</CardContent>
|
|
82
85
|
</Card>
|
|
83
86
|
);
|
|
@@ -86,7 +89,7 @@ export function CustomerInfoCard({ customer, loading, error }: CustomerInfoCardP
|
|
|
86
89
|
return (
|
|
87
90
|
<Card>
|
|
88
91
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
|
89
|
-
<CardTitle>
|
|
92
|
+
<CardTitle>{t("billing.ui.cards.customer.title")}</CardTitle>
|
|
90
93
|
<User className="h-4 w-4 text-muted-foreground" />
|
|
91
94
|
</CardHeader>
|
|
92
95
|
<CardContent>
|
|
@@ -95,14 +98,14 @@ export function CustomerInfoCard({ customer, loading, error }: CustomerInfoCardP
|
|
|
95
98
|
{customer.email && <p className="text-sm text-muted-foreground">{customer.email}</p>}
|
|
96
99
|
{customer.balance !== undefined && customer.balance !== 0 && (
|
|
97
100
|
<p className="text-sm">
|
|
98
|
-
<span className="text-muted-foreground">
|
|
101
|
+
<span className="text-muted-foreground">{t("billing.ui.cards.customer.credit_balance")} </span>
|
|
99
102
|
<span className={customer.balance < 0 ? "text-success" : "text-destructive"}>
|
|
100
|
-
{formatBalance(customer.balance, customer.currency)}
|
|
103
|
+
{formatBalance(customer.balance, customer.currency, locale)}
|
|
101
104
|
</span>
|
|
102
105
|
</p>
|
|
103
106
|
)}
|
|
104
107
|
<Button variant="outline" size="sm" className="mt-2" onClick={handlePortalClick} disabled={portalLoading}>
|
|
105
|
-
{portalLoading ? "
|
|
108
|
+
{portalLoading ? t("common.loading") : t("billing.ui.cards.customer.manage_portal")}
|
|
106
109
|
<ExternalLink className="h-4 w-4 ms-1" />
|
|
107
110
|
</Button>
|
|
108
111
|
</div>
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
+
import { useTranslations } from "next-intl";
|
|
3
4
|
import { ChevronRight, ReceiptIcon } from "lucide-react";
|
|
4
5
|
import { Badge, Card, CardContent, CardHeader, CardTitle, Skeleton } from "../../../../shadcnui";
|
|
5
6
|
import { InvoiceStatus, StripeInvoiceInterface } from "../../stripe-invoice";
|
|
7
|
+
import { formatDate, useBillingFormatOptions } from "../utils";
|
|
6
8
|
|
|
7
9
|
type InvoicesSummaryCardProps = {
|
|
8
10
|
invoices: StripeInvoiceInterface[];
|
|
@@ -11,6 +13,9 @@ type InvoicesSummaryCardProps = {
|
|
|
11
13
|
onViewAllClick: () => void;
|
|
12
14
|
};
|
|
13
15
|
|
|
16
|
+
/** Statuses with a `billing.ui.invoice_status.*` label; anything else Stripe adds is shown raw. */
|
|
17
|
+
const INVOICE_STATUSES: string[] = Object.values(InvoiceStatus);
|
|
18
|
+
|
|
14
19
|
function getStatusBadgeVariant(status: InvoiceStatus): "default" | "secondary" | "destructive" | "outline" {
|
|
15
20
|
switch (status) {
|
|
16
21
|
case InvoiceStatus.PAID:
|
|
@@ -25,28 +30,22 @@ function getStatusBadgeVariant(status: InvoiceStatus): "default" | "secondary" |
|
|
|
25
30
|
}
|
|
26
31
|
}
|
|
27
32
|
|
|
28
|
-
function
|
|
29
|
-
return new Date(date).toLocaleDateString(undefined, {
|
|
30
|
-
year: "numeric",
|
|
31
|
-
month: "short",
|
|
32
|
-
day: "numeric",
|
|
33
|
-
});
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
function formatAmount(amount: number, currency: string): string {
|
|
33
|
+
function formatAmount(amount: number, currency: string, locale: string): string {
|
|
37
34
|
const currencyCode = currency?.toUpperCase() || "USD";
|
|
38
|
-
return new Intl.NumberFormat(
|
|
35
|
+
return new Intl.NumberFormat(locale, {
|
|
39
36
|
style: "currency",
|
|
40
37
|
currency: currencyCode,
|
|
41
38
|
}).format(amount / 100);
|
|
42
39
|
}
|
|
43
40
|
|
|
44
41
|
export function InvoicesSummaryCard({ invoices, loading, error, onViewAllClick }: InvoicesSummaryCardProps) {
|
|
42
|
+
const t = useTranslations();
|
|
43
|
+
const fmt = useBillingFormatOptions();
|
|
45
44
|
if (loading) {
|
|
46
45
|
return (
|
|
47
46
|
<Card>
|
|
48
47
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
|
49
|
-
<CardTitle>
|
|
48
|
+
<CardTitle>{t("billing.ui.cards.invoices.title")}</CardTitle>
|
|
50
49
|
<ReceiptIcon className="h-4 w-4 text-muted-foreground" />
|
|
51
50
|
</CardHeader>
|
|
52
51
|
<CardContent>
|
|
@@ -62,7 +61,7 @@ export function InvoicesSummaryCard({ invoices, loading, error, onViewAllClick }
|
|
|
62
61
|
return (
|
|
63
62
|
<Card>
|
|
64
63
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
|
65
|
-
<CardTitle>
|
|
64
|
+
<CardTitle>{t("billing.ui.cards.invoices.title")}</CardTitle>
|
|
66
65
|
<ReceiptIcon className="h-4 w-4 text-muted-foreground" />
|
|
67
66
|
</CardHeader>
|
|
68
67
|
<CardContent>
|
|
@@ -80,29 +79,42 @@ export function InvoicesSummaryCard({ invoices, loading, error, onViewAllClick }
|
|
|
80
79
|
return (
|
|
81
80
|
<Card className="cursor-pointer hover:bg-accent/50 transition-colors" onClick={onViewAllClick}>
|
|
82
81
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
|
83
|
-
<CardTitle>
|
|
82
|
+
<CardTitle>{t("billing.ui.cards.invoices.title")}</CardTitle>
|
|
84
83
|
<ReceiptIcon className="h-4 w-4 text-muted-foreground" />
|
|
85
84
|
</CardHeader>
|
|
86
85
|
<CardContent>
|
|
87
86
|
{invoices.length === 0 ? (
|
|
88
87
|
<div className="space-y-2">
|
|
89
|
-
<p className="text-xl font-bold text-muted-foreground">
|
|
90
|
-
<p className="text-xs text-muted-foreground">
|
|
88
|
+
<p className="text-xl font-bold text-muted-foreground">{t("billing.ui.invoices.empty_title")}</p>
|
|
89
|
+
<p className="text-xs text-muted-foreground">{t("billing.ui.cards.invoices.empty_description")}</p>
|
|
91
90
|
</div>
|
|
92
91
|
) : latestInvoice ? (
|
|
93
92
|
<div className="space-y-2">
|
|
94
93
|
<div className="flex items-center gap-2">
|
|
95
|
-
<p className="text-xl font-bold">
|
|
96
|
-
|
|
94
|
+
<p className="text-xl font-bold">
|
|
95
|
+
{formatAmount(latestInvoice.total, latestInvoice.currency, fmt.locale)}
|
|
96
|
+
</p>
|
|
97
|
+
<Badge variant={getStatusBadgeVariant(latestInvoice.status)}>
|
|
98
|
+
{INVOICE_STATUSES.includes(latestInvoice.status)
|
|
99
|
+
? t(`billing.ui.invoice_status.${latestInvoice.status}`)
|
|
100
|
+
: latestInvoice.status}
|
|
101
|
+
</Badge>
|
|
97
102
|
</div>
|
|
98
103
|
<p className="text-sm text-muted-foreground">
|
|
99
|
-
{latestInvoice.stripeInvoiceNumber ||
|
|
104
|
+
{latestInvoice.stripeInvoiceNumber ||
|
|
105
|
+
t("billing.ui.cards.invoices.invoice_from", { date: formatDate(latestInvoice.periodStart, fmt) })}
|
|
100
106
|
</p>
|
|
101
107
|
<div className="flex items-center gap-4 text-xs text-muted-foreground">
|
|
102
|
-
{paidInvoices.length > 0 &&
|
|
103
|
-
|
|
108
|
+
{paidInvoices.length > 0 && (
|
|
109
|
+
<span>{t("billing.ui.cards.invoices.paid_count", { count: paidInvoices.length })}</span>
|
|
110
|
+
)}
|
|
111
|
+
{openInvoices.length > 0 && (
|
|
112
|
+
<span className="text-warning">
|
|
113
|
+
{t("billing.ui.cards.invoices.open_count", { count: openInvoices.length })}
|
|
114
|
+
</span>
|
|
115
|
+
)}
|
|
104
116
|
<span className="flex items-center">
|
|
105
|
-
|
|
117
|
+
{t("billing.ui.cards.view_all")}
|
|
106
118
|
<ChevronRight className="h-3 w-3 ms-1 rtl:rotate-180" />
|
|
107
119
|
</span>
|
|
108
120
|
</div>
|