@fayz-ai/plugin-courses 0.9.0 → 0.10.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/README.md +3 -3
- package/dist/components/CommerceUI.d.ts +19 -2
- package/dist/components/CommerceUI.d.ts.map +1 -1
- package/dist/components/dashboardWidgets.d.ts.map +1 -0
- package/dist/index.d.ts +26 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +773 -309
- package/dist/index.js.map +1 -1
- package/dist/integrations/kiwify/connectorDef.d.ts +34 -0
- package/dist/integrations/kiwify/connectorDef.d.ts.map +1 -0
- package/dist/integrations/kiwify/data.d.ts +58 -0
- package/dist/integrations/kiwify/data.d.ts.map +1 -0
- package/dist/lib/nav.d.ts.map +1 -0
- package/dist/locales/en.d.ts.map +1 -1
- package/dist/locales/pt-BR.d.ts.map +1 -1
- package/dist/public/createPublicCheckoutPlugin.d.ts +1 -1
- package/dist/public/createPublicCheckoutPlugin.d.ts.map +1 -1
- package/dist/public/{data.d.ts → data/index.d.ts} +1 -1
- package/dist/public/data/index.d.ts.map +1 -0
- package/dist/public/{data.supabase.d.ts → data/supabase.d.ts} +2 -2
- package/dist/public/data/supabase.d.ts.map +1 -0
- package/dist/public/index.d.ts +4 -4
- package/dist/public/index.d.ts.map +1 -1
- package/dist/public/index.js +353 -0
- package/dist/public/index.js.map +1 -0
- package/dist/public/{CheckoutScreen.d.ts → views/CheckoutScreen.d.ts} +1 -1
- package/dist/public/views/CheckoutScreen.d.ts.map +1 -0
- package/dist/views/CourseEditorPage.d.ts.map +1 -0
- package/dist/views/CourseOrderDetailView.d.ts +4 -3
- package/dist/views/CourseOrderDetailView.d.ts.map +1 -1
- package/dist/{pages → views}/CoursesListPage.d.ts.map +1 -1
- package/dist/views/FinancialPage.d.ts.map +1 -0
- package/dist/{pages → views}/ReportsPage.d.ts.map +1 -1
- package/dist/views/SalesPage.d.ts +19 -0
- package/dist/views/SalesPage.d.ts.map +1 -0
- package/dist/views/SubscriptionsPage.d.ts +7 -0
- package/dist/views/SubscriptionsPage.d.ts.map +1 -0
- package/package.json +12 -12
- package/dist/connectors/stripe.d.ts +0 -3
- package/dist/connectors/stripe.d.ts.map +0 -1
- package/dist/dashboardWidgets.d.ts.map +0 -1
- package/dist/nav.d.ts.map +0 -1
- package/dist/pages/CourseEditorPage.d.ts.map +0 -1
- package/dist/pages/FinancialPage.d.ts.map +0 -1
- package/dist/pages/SalesPage.d.ts +0 -6
- package/dist/pages/SalesPage.d.ts.map +0 -1
- package/dist/pages/SubscriptionsPage.d.ts +0 -6
- package/dist/pages/SubscriptionsPage.d.ts.map +0 -1
- package/dist/public/CheckoutScreen.d.ts.map +0 -1
- package/dist/public/data.d.ts.map +0 -1
- package/dist/public/data.supabase.d.ts.map +0 -1
- package/src/components/CommerceUI.tsx +0 -153
- package/src/connectors/stripe.ts +0 -34
- package/src/dashboardWidgets.tsx +0 -64
- package/src/index.ts +0 -150
- package/src/lib/format.ts +0 -22
- package/src/locales/en.ts +0 -203
- package/src/locales/index.ts +0 -7
- package/src/locales/pt-BR.ts +0 -203
- package/src/nav.ts +0 -9
- package/src/pages/CourseEditorPage.tsx +0 -1122
- package/src/pages/CoursesListPage.tsx +0 -137
- package/src/pages/FinancialPage.tsx +0 -81
- package/src/pages/ReportsPage.tsx +0 -59
- package/src/pages/SalesPage.tsx +0 -86
- package/src/pages/SubscriptionsPage.tsx +0 -56
- package/src/public/CheckoutScreen.tsx +0 -279
- package/src/public/createPublicCheckoutPlugin.tsx +0 -88
- package/src/public/data.supabase.ts +0 -63
- package/src/public/data.ts +0 -89
- package/src/public/index.ts +0 -26
- package/src/settings/CoursesSettings.tsx +0 -55
- package/src/views/CourseOrderDetailView.tsx +0 -132
- /package/dist/{dashboardWidgets.d.ts → components/dashboardWidgets.d.ts} +0 -0
- /package/dist/{nav.d.ts → lib/nav.d.ts} +0 -0
- /package/dist/{pages → views}/CourseEditorPage.d.ts +0 -0
- /package/dist/{pages → views}/CoursesListPage.d.ts +0 -0
- /package/dist/{pages → views}/FinancialPage.d.ts +0 -0
- /package/dist/{pages → views}/ReportsPage.d.ts +0 -0
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import * as React from 'react';
|
|
2
|
-
/** Order detail (
|
|
3
|
-
* SHARED archetype OrderDetailView (@fayz-ai/
|
|
4
|
-
export declare function CourseOrderDetailView({ id }: {
|
|
2
|
+
/** Order detail (`${ordersRoute}/:id`). Loads the canonical order and renders the
|
|
3
|
+
* SHARED archetype OrderDetailView (@fayz-ai/admin) — same component the shop uses. */
|
|
4
|
+
export declare function CourseOrderDetailView({ id, ordersRoute }: {
|
|
5
5
|
id?: string;
|
|
6
|
+
ordersRoute?: string;
|
|
6
7
|
}): React.JSX.Element;
|
|
7
8
|
export declare namespace CourseOrderDetailView {
|
|
8
9
|
var displayName: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CourseOrderDetailView.d.ts","sourceRoot":"","sources":["../../src/views/CourseOrderDetailView.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AA+B9B;
|
|
1
|
+
{"version":3,"file":"CourseOrderDetailView.d.ts","sourceRoot":"","sources":["../../src/views/CourseOrderDetailView.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AA+B9B;wFACwF;AACxF,wBAAgB,qBAAqB,CAAC,EAAE,EAAE,EAAE,WAA8B,EAAE,EAAE;IAAE,EAAE,CAAC,EAAE,MAAM,CAAC;IAAC,WAAW,CAAC,EAAE,MAAM,CAAA;CAAE,qBAoGlH;yBApGe,qBAAqB"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CoursesListPage.d.ts","sourceRoot":"","sources":["../../src/
|
|
1
|
+
{"version":3,"file":"CoursesListPage.d.ts","sourceRoot":"","sources":["../../src/views/CoursesListPage.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAa9B,wBAAgB,eAAe,sBAyG9B;yBAzGe,eAAe"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"FinancialPage.d.ts","sourceRoot":"","sources":["../../src/views/FinancialPage.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAY9B,wBAAgB,aAAa,sBAgD5B;yBAhDe,aAAa"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ReportsPage.d.ts","sourceRoot":"","sources":["../../src/
|
|
1
|
+
{"version":3,"file":"ReportsPage.d.ts","sourceRoot":"","sources":["../../src/views/ReportsPage.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAc9B,wBAAgB,WAAW,sBA2B1B;yBA3Be,WAAW"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
/** Uma aba da faixa que Pedidos e Assinaturas dividem (ver `salesTabs`). */
|
|
3
|
+
export interface SalesTab {
|
|
4
|
+
value: string;
|
|
5
|
+
label: string;
|
|
6
|
+
route: string;
|
|
7
|
+
}
|
|
8
|
+
export interface SalesTabProps {
|
|
9
|
+
/** Construída por chamada: navegar depende do roteador. */
|
|
10
|
+
navTabs?: () => SalesTab[];
|
|
11
|
+
activeNavTab?: string;
|
|
12
|
+
/** Base do detalhe do pedido (`${ordersRoute}/:id`). */
|
|
13
|
+
ordersRoute?: string;
|
|
14
|
+
}
|
|
15
|
+
export declare function SalesPage({ navTabs, activeNavTab, ordersRoute }?: SalesTabProps): React.JSX.Element;
|
|
16
|
+
export declare namespace SalesPage {
|
|
17
|
+
var displayName: string;
|
|
18
|
+
}
|
|
19
|
+
//# sourceMappingURL=SalesPage.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"SalesPage.d.ts","sourceRoot":"","sources":["../../src/views/SalesPage.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAmB9B,4EAA4E;AAC5E,MAAM,WAAW,QAAQ;IACvB,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,MAAM,CAAA;CACd;AAED,MAAM,WAAW,aAAa;IAC5B,2DAA2D;IAC3D,OAAO,CAAC,EAAE,MAAM,QAAQ,EAAE,CAAA;IAC1B,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,wDAAwD;IACxD,WAAW,CAAC,EAAE,MAAM,CAAA;CACrB;AAED,wBAAgB,SAAS,CAAC,EAAE,OAAO,EAAE,YAAY,EAAE,WAA8B,EAAE,GAAE,aAAkB,qBAwEtG;yBAxEe,SAAS"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import type { SalesTabProps } from './SalesPage';
|
|
3
|
+
export declare function SubscriptionsPage({ navTabs, activeNavTab }?: SalesTabProps): React.JSX.Element;
|
|
4
|
+
export declare namespace SubscriptionsPage {
|
|
5
|
+
var displayName: string;
|
|
6
|
+
}
|
|
7
|
+
//# sourceMappingURL=SubscriptionsPage.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"SubscriptionsPage.d.ts","sourceRoot":"","sources":["../../src/views/SubscriptionsPage.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAO9B,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAA;AAQhD,wBAAgB,iBAAiB,CAAC,EAAE,OAAO,EAAE,YAAY,EAAE,GAAE,aAAkB,qBAmD9E;yBAnDe,iBAAiB"}
|
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"fayz": {
|
|
4
4
|
"status": "preview"
|
|
5
5
|
},
|
|
6
|
-
"version": "0.
|
|
6
|
+
"version": "0.10.0",
|
|
7
7
|
"description": "[experimental] Fayz SDK — courses admin management plugin",
|
|
8
8
|
"type": "module",
|
|
9
9
|
"sideEffects": false,
|
|
@@ -12,19 +12,16 @@
|
|
|
12
12
|
"types": "./dist/index.d.ts",
|
|
13
13
|
"exports": {
|
|
14
14
|
".": {
|
|
15
|
-
"source": "./src/index.ts",
|
|
16
15
|
"types": "./dist/index.d.ts",
|
|
17
16
|
"import": "./dist/index.js"
|
|
18
17
|
},
|
|
19
18
|
"./public": {
|
|
20
|
-
"source": "./src/public/index.ts",
|
|
21
19
|
"types": "./dist/public/index.d.ts",
|
|
22
20
|
"import": "./dist/public/index.js"
|
|
23
21
|
}
|
|
24
22
|
},
|
|
25
23
|
"files": [
|
|
26
|
-
"dist"
|
|
27
|
-
"src"
|
|
24
|
+
"dist"
|
|
28
25
|
],
|
|
29
26
|
"peerDependencies": {
|
|
30
27
|
"react": "^18.0.0 || ^19.0.0",
|
|
@@ -36,17 +33,18 @@
|
|
|
36
33
|
"@dnd-kit/core": "^6.3.1",
|
|
37
34
|
"@dnd-kit/sortable": "^10.0.0",
|
|
38
35
|
"@dnd-kit/utilities": "^3.2.2",
|
|
39
|
-
"@fayz-ai/core": "^0.
|
|
40
|
-
"@fayz-ai/ui": "^0.
|
|
41
|
-
"@fayz-ai/courses": "^0.
|
|
42
|
-
"@fayz-ai/
|
|
36
|
+
"@fayz-ai/core": "^0.11.0",
|
|
37
|
+
"@fayz-ai/ui": "^0.11.0",
|
|
38
|
+
"@fayz-ai/courses": "^0.11.0",
|
|
39
|
+
"@fayz-ai/admin": "^0.11.0"
|
|
43
40
|
},
|
|
44
41
|
"devDependencies": {
|
|
45
42
|
"@types/react": "^18.3.0",
|
|
46
43
|
"react": "^18.3.0",
|
|
47
44
|
"tsup": "^8.2.0",
|
|
48
45
|
"typescript": "^5.5.0",
|
|
49
|
-
"lucide-react": "^0.400.0"
|
|
46
|
+
"lucide-react": "^0.400.0",
|
|
47
|
+
"vitest": "^4.1.5"
|
|
50
48
|
},
|
|
51
49
|
"license": "MIT",
|
|
52
50
|
"keywords": [
|
|
@@ -61,7 +59,9 @@
|
|
|
61
59
|
"scripts": {
|
|
62
60
|
"build": "tsup && tsc --emitDeclarationOnly --declaration --declarationMap --noEmit false",
|
|
63
61
|
"dev": "tsup --watch",
|
|
64
|
-
"typecheck": "tsc --noEmit",
|
|
65
|
-
"clean": "rm -rf dist"
|
|
62
|
+
"typecheck": "tsc --noEmit -p tsconfig.test.json",
|
|
63
|
+
"clean": "rm -rf dist",
|
|
64
|
+
"test": "vitest run && pnpm run test:functions",
|
|
65
|
+
"test:functions": "node --test functions/_shared/*.test.mjs"
|
|
66
66
|
}
|
|
67
67
|
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"stripe.d.ts","sourceRoot":"","sources":["../../src/connectors/stripe.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,eAAe,CAAA;AAMxD,wBAAgB,qBAAqB,IAAI,mBAAmB,CA2B3D"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"dashboardWidgets.d.ts","sourceRoot":"","sources":["../src/dashboardWidgets.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,eAAe,CAAA;AA0CvD;;2EAE2E;AAC3E,wBAAgB,6BAA6B,CAC3C,OAAO,GAAE;IAAE,KAAK,CAAC,EAAE,OAAO,CAAC;IAAC,aAAa,CAAC,EAAE,OAAO,CAAC;IAAC,SAAS,CAAC,EAAE,OAAO,CAAA;CAAO,GAC9E,kBAAkB,EAAE,CAetB"}
|
package/dist/nav.d.ts.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"nav.d.ts","sourceRoot":"","sources":["../src/nav.ts"],"names":[],"mappings":"AAMA,wBAAgB,UAAU,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI,CAE3C"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"CourseEditorPage.d.ts","sourceRoot":"","sources":["../../src/pages/CourseEditorPage.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAqF9B,wBAAgB,gBAAgB,CAAC,EAAE,EAAE,EAAE,EAAE;IAAE,EAAE,CAAC,EAAE,MAAM,CAAA;CAAE,qBAyiBvD;yBAziBe,gBAAgB"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"FinancialPage.d.ts","sourceRoot":"","sources":["../../src/pages/FinancialPage.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAY9B,wBAAgB,aAAa,sBAmE5B;yBAnEe,aAAa"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"SalesPage.d.ts","sourceRoot":"","sources":["../../src/pages/SalesPage.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAmB9B,wBAAgB,SAAS,sBAiExB;yBAjEe,SAAS"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"SubscriptionsPage.d.ts","sourceRoot":"","sources":["../../src/pages/SubscriptionsPage.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAa9B,wBAAgB,iBAAiB,sBAyChC;yBAzCe,iBAAiB"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"CheckoutScreen.d.ts","sourceRoot":"","sources":["../../src/public/CheckoutScreen.tsx"],"names":[],"mappings":"AAQA,OAAO,EAAgC,KAAK,EAAE,EAAkB,MAAM,OAAO,CAAA;AAG7E,OAAO,KAAK,EAAyB,0BAA0B,EAAE,MAAM,QAAQ,CAAA;AAE/E,MAAM,WAAW,mBAAmB;IAClC,IAAI,EAAE,MAAM,CAAA;IACZ,QAAQ,EAAE,0BAA0B,CAAA;IACpC,kEAAkE;IAClE,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,0EAA0E;IAC1E,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,0EAA0E;IAC1E,SAAS,CAAC,EAAE,CAAC,IAAI,EAAE;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,KAAK,IAAI,CAAA;CAC/D;AAUD,eAAO,MAAM,cAAc,EAAE,EAAE,CAAC,mBAAmB,CAoPlD,CAAA"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"data.d.ts","sourceRoot":"","sources":["../../src/public/data.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,kBAAkB,CAAA;AAErD,MAAM,MAAM,qBAAqB,GAAG,KAAK,GAAG,MAAM,CAAA;AAElD,MAAM,WAAW,eAAe;IAC9B,KAAK,EAAE,MAAM,CAAA;IACb,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,aAAa,CAAC,EAAE,qBAAqB,CAAA;CACtC;AAED,MAAM,WAAW,gBAAgB;IAC/B,OAAO,EAAE,MAAM,CAAA;CAChB;AAED,MAAM,WAAW,0BAA0B;IACzC,6EAA6E;IAC7E,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAA;IACrD,0EAA0E;IAC1E,eAAe,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,KAAK,GAAG,IAAI,CAAC,CAAA;IACxD,4EAA4E;IAC5E,UAAU,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,eAAe,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAAA;CAC/E;AAED,MAAM,WAAW,mBAAmB;IAClC,MAAM,CAAC,EAAE,OAAO,CAAC,MAAM,CAAC,CAAA;IACxB,KAAK,CAAC,EAAE,OAAO,CAAC,KAAK,CAAC,CAAA;CACvB;AAED;;;GAGG;AACH,wBAAgB,0BAA0B,CAAC,IAAI,GAAE,mBAAwB,GAAG,0BAA0B,CA8CrG"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"data.supabase.d.ts","sourceRoot":"","sources":["../../src/public/data.supabase.ts"],"names":[],"mappings":"AAeA,OAAO,KAAK,EAAE,0BAA0B,EAAmB,MAAM,QAAQ,CAAA;AAEzE,MAAM,WAAW,uBAAuB;IACtC,2DAA2D;IAC3D,QAAQ,EAAE,MAAM,CAAA;CACjB;AAYD,wBAAgB,8BAA8B,CAAC,OAAO,EAAE,uBAAuB,GAAG,0BAA0B,CA8B3G"}
|
|
@@ -1,153 +0,0 @@
|
|
|
1
|
-
import * as React from 'react'
|
|
2
|
-
import type { ColumnDef } from '@tanstack/react-table'
|
|
3
|
-
import { Card, DataTable } from '@fayz-ai/ui'
|
|
4
|
-
|
|
5
|
-
/** Standard padded container for the commerce admin pages. */
|
|
6
|
-
export function PageContainer({ title, subtitle, actions, children }: {
|
|
7
|
-
title: string
|
|
8
|
-
subtitle?: string
|
|
9
|
-
actions?: React.ReactNode
|
|
10
|
-
children: React.ReactNode
|
|
11
|
-
}) {
|
|
12
|
-
return (
|
|
13
|
-
<div className="mx-auto max-w-6xl p-6 md:p-8">
|
|
14
|
-
<div className="mb-6 flex items-start justify-between gap-4">
|
|
15
|
-
<div>
|
|
16
|
-
<h1 className="text-2xl font-bold text-foreground">{title}</h1>
|
|
17
|
-
{subtitle && <p className="mt-1 text-sm text-muted-foreground">{subtitle}</p>}
|
|
18
|
-
</div>
|
|
19
|
-
{actions}
|
|
20
|
-
</div>
|
|
21
|
-
{children}
|
|
22
|
-
</div>
|
|
23
|
-
)
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
/** A single KPI tile (revenue, sales, MRR…). */
|
|
27
|
-
export function StatCard({ label, value, hint }: { label: string; value: string; hint?: string }) {
|
|
28
|
-
return (
|
|
29
|
-
<Card className="p-5">
|
|
30
|
-
<p className="text-sm text-muted-foreground">{label}</p>
|
|
31
|
-
<p className="mt-1 text-2xl font-bold text-foreground">{value}</p>
|
|
32
|
-
{hint && <p className="mt-1 text-xs text-muted-foreground">{hint}</p>}
|
|
33
|
-
</Card>
|
|
34
|
-
)
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
export function StatGrid({ children }: { children: React.ReactNode }) {
|
|
38
|
-
return <div className="mb-6 grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4">{children}</div>
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/** Empty/loading state for the tables. */
|
|
42
|
-
export function TableEmpty({ children }: { children: React.ReactNode }) {
|
|
43
|
-
return (
|
|
44
|
-
<div className="rounded-xl border border-dashed border-border p-12 text-center text-sm text-muted-foreground">
|
|
45
|
-
{children}
|
|
46
|
-
</div>
|
|
47
|
-
)
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* Standard empty state — an icon chip, a title, an optional description and a
|
|
52
|
-
* call-to-action. Use anywhere a list/section can be legitimately empty so the
|
|
53
|
-
* screen never shows a bare dashed box.
|
|
54
|
-
*/
|
|
55
|
-
export function EmptyState({ icon, title, description, action }: {
|
|
56
|
-
icon?: React.ReactNode
|
|
57
|
-
title: string
|
|
58
|
-
description?: string
|
|
59
|
-
action?: React.ReactNode
|
|
60
|
-
}) {
|
|
61
|
-
return (
|
|
62
|
-
<div className="flex flex-col items-center justify-center rounded-xl border border-dashed border-border bg-card/40 px-6 py-12 text-center">
|
|
63
|
-
{icon && (
|
|
64
|
-
<div className="mb-3 flex h-11 w-11 items-center justify-center rounded-full bg-muted text-muted-foreground">
|
|
65
|
-
{icon}
|
|
66
|
-
</div>
|
|
67
|
-
)}
|
|
68
|
-
<p className="text-sm font-medium text-foreground">{title}</p>
|
|
69
|
-
{description && <p className="mt-1 max-w-sm text-sm text-muted-foreground">{description}</p>}
|
|
70
|
-
{action && <div className="mt-4">{action}</div>}
|
|
71
|
-
</div>
|
|
72
|
-
)
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
/**
|
|
76
|
-
* A muted, low-emphasis informational callout. Use to set honest expectations
|
|
77
|
-
* (e.g. "this applies to a feature that isn't shipped yet") without dressing it
|
|
78
|
-
* up as an interactive control. Wrap key phrases in <strong> for emphasis.
|
|
79
|
-
*/
|
|
80
|
-
export function InfoNote({ icon, children, className }: {
|
|
81
|
-
icon?: React.ReactNode
|
|
82
|
-
children: React.ReactNode
|
|
83
|
-
className?: string
|
|
84
|
-
}) {
|
|
85
|
-
return (
|
|
86
|
-
<div className={'flex items-start gap-2.5 rounded-lg border border-border bg-muted/40 px-3.5 py-3 text-sm text-muted-foreground ' + (className ?? '')}>
|
|
87
|
-
{icon && <span className="mt-0.5 shrink-0 text-muted-foreground/80">{icon}</span>}
|
|
88
|
-
<div className="min-w-0 [&_strong]:font-medium [&_strong]:text-foreground">{children}</div>
|
|
89
|
-
</div>
|
|
90
|
-
)
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
/** A titled card section — the house pattern for grouping form fields. */
|
|
94
|
-
export function SectionCard({ title, description, actions, children, className }: {
|
|
95
|
-
title?: string
|
|
96
|
-
description?: string
|
|
97
|
-
actions?: React.ReactNode
|
|
98
|
-
children: React.ReactNode
|
|
99
|
-
className?: string
|
|
100
|
-
}) {
|
|
101
|
-
return (
|
|
102
|
-
<Card className={'p-5 md:p-6 ' + (className ?? '')}>
|
|
103
|
-
{(title || actions) && (
|
|
104
|
-
<div className="mb-4 flex items-start justify-between gap-4">
|
|
105
|
-
<div>
|
|
106
|
-
{title && <h2 className="text-sm font-semibold text-foreground">{title}</h2>}
|
|
107
|
-
{description && <p className="mt-0.5 text-sm text-muted-foreground">{description}</p>}
|
|
108
|
-
</div>
|
|
109
|
-
{actions}
|
|
110
|
-
</div>
|
|
111
|
-
)}
|
|
112
|
-
{children}
|
|
113
|
-
</Card>
|
|
114
|
-
)
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
export interface SimpleColumn<T> {
|
|
118
|
-
key: string
|
|
119
|
-
header: string
|
|
120
|
-
render: (row: T) => React.ReactNode
|
|
121
|
-
align?: 'left' | 'right'
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
/**
|
|
125
|
-
* Thin adapter over the design-system DataTable (@fayz-ai/ui): the commerce
|
|
126
|
-
* pages describe columns declaratively; this maps them to tanstack ColumnDef so
|
|
127
|
-
* we reuse DataTable's sorting/skeleton/empty chrome instead of a bespoke table.
|
|
128
|
-
*/
|
|
129
|
-
export function SimpleTable<T>({ columns, rows, empty, loading, onRowClick }: {
|
|
130
|
-
columns: SimpleColumn<T>[]
|
|
131
|
-
rows: T[]
|
|
132
|
-
empty: string
|
|
133
|
-
loading?: boolean
|
|
134
|
-
onRowClick?: (row: T) => void
|
|
135
|
-
}) {
|
|
136
|
-
const cols = React.useMemo<ColumnDef<T>[]>(
|
|
137
|
-
() => columns.map((c) => ({
|
|
138
|
-
id: c.key,
|
|
139
|
-
header: () => <div className={c.align === 'right' ? 'text-right' : ''}>{c.header}</div>,
|
|
140
|
-
cell: ({ row }) => <div className={c.align === 'right' ? 'text-right' : ''}>{c.render(row.original)}</div>,
|
|
141
|
-
})),
|
|
142
|
-
[columns],
|
|
143
|
-
)
|
|
144
|
-
return (
|
|
145
|
-
<DataTable
|
|
146
|
-
columns={cols}
|
|
147
|
-
data={rows}
|
|
148
|
-
emptyMessage={empty}
|
|
149
|
-
loading={loading}
|
|
150
|
-
onRowClick={onRowClick ? (row: T) => onRowClick(row) : undefined}
|
|
151
|
-
/>
|
|
152
|
-
)
|
|
153
|
-
}
|
package/src/connectors/stripe.ts
DELETED
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
import type { ConnectorDefinition } from '@fayz-ai/core'
|
|
2
|
-
import { getCoursesProvider } from '@fayz-ai/courses'
|
|
3
|
-
|
|
4
|
-
// Stripe Connect modeled as a connector so it surfaces in the plugin's unified
|
|
5
|
-
// "Integrations" settings tab (same pattern as Financial Settings → Integrações),
|
|
6
|
-
// instead of a bespoke block in the General tab. The host plugin is 'courses'.
|
|
7
|
-
export function createStripeConnector(): ConnectorDefinition {
|
|
8
|
-
return {
|
|
9
|
-
id: 'stripe-connect',
|
|
10
|
-
hostPluginId: 'courses',
|
|
11
|
-
name: 'Stripe',
|
|
12
|
-
description: 'Receba os pagamentos dos alunos; a taxa da plataforma é aplicada automaticamente.',
|
|
13
|
-
icon: 'CreditCard',
|
|
14
|
-
authKind: 'oauth',
|
|
15
|
-
async getStatus() {
|
|
16
|
-
try {
|
|
17
|
-
const account = await getCoursesProvider().getCreatorAccount()
|
|
18
|
-
const fee = (account.platformFeeBps / 100).toFixed(2)
|
|
19
|
-
return {
|
|
20
|
-
connected: !!account.stripeAccountId && account.chargesEnabled,
|
|
21
|
-
detail: `Taxa da plataforma: ${fee}%`,
|
|
22
|
-
}
|
|
23
|
-
} catch {
|
|
24
|
-
return { connected: false }
|
|
25
|
-
}
|
|
26
|
-
},
|
|
27
|
-
async startOAuth(_redirectTo?: string): Promise<string> {
|
|
28
|
-
// Stage 3 wires this to the `courses-connect-onboard` edge function and
|
|
29
|
-
// returns the Stripe account-link URL to redirect to. Until then, surface
|
|
30
|
-
// a clear message (ConnectorsHub toasts a thrown error's message).
|
|
31
|
-
throw new Error('O onboarding do Stripe Connect é ligado na Etapa 3.')
|
|
32
|
-
},
|
|
33
|
-
}
|
|
34
|
-
}
|
package/src/dashboardWidgets.tsx
DELETED
|
@@ -1,64 +0,0 @@
|
|
|
1
|
-
import * as React from 'react'
|
|
2
|
-
import type { DashboardWidgetDef } from '@fayz-ai/core'
|
|
3
|
-
import { useTranslation } from '@fayz-ai/core'
|
|
4
|
-
import { KpiCard, defineKpiWidget } from '@fayz-ai/ui'
|
|
5
|
-
import { getCoursesProvider, type FinancialSummary } from '@fayz-ai/courses'
|
|
6
|
-
import { formatMoney, formatPercent } from './lib/format'
|
|
7
|
-
|
|
8
|
-
// Course KPIs contributed to the central admin Dashboard (rendered by
|
|
9
|
-
// @fayz-ai/plugin-dashboard's DashboardCanvas, which auto-collects every
|
|
10
|
-
// plugin's dashboardWidgets for surface 'home'). Mirrors Kiwify's dashboard.
|
|
11
|
-
|
|
12
|
-
function useSummary(): FinancialSummary | null {
|
|
13
|
-
const [summary, setSummary] = React.useState<FinancialSummary | null>(null)
|
|
14
|
-
React.useEffect(() => {
|
|
15
|
-
void getCoursesProvider().getFinancialSummary().then(setSummary)
|
|
16
|
-
}, [])
|
|
17
|
-
return summary
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
function RevenueKpi() {
|
|
21
|
-
const t = useTranslation()
|
|
22
|
-
const s = useSummary()
|
|
23
|
-
return <KpiCard label={t('courses.kpi.revenue') || 'Total revenue'} icon="DollarSign" loading={!s} value={s ? formatMoney(s.totalRevenue, s.currency) : undefined} />
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
function SalesKpi() {
|
|
27
|
-
const t = useTranslation()
|
|
28
|
-
const s = useSummary()
|
|
29
|
-
return <KpiCard label={t('courses.kpi.sales') || 'Sales'} icon="TrendingUp" loading={!s} value={s ? String(s.salesCount) : undefined} sub={s ? formatPercent(s.refundRate, 1) + ' ' + (t('courses.kpi.refunds') || 'refunds') : undefined} />
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
function MrrKpi() {
|
|
33
|
-
const t = useTranslation()
|
|
34
|
-
const s = useSummary()
|
|
35
|
-
return <KpiCard label={t('courses.kpi.mrr') || 'MRR'} icon="Repeat" loading={!s} value={s ? formatMoney(s.mrr, s.currency) : undefined} sub={s ? `${s.activeSubscriptions} ${t('courses.kpi.activeSubs') || 'active'}` : undefined} />
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
function FeesKpi() {
|
|
39
|
-
const t = useTranslation()
|
|
40
|
-
const s = useSummary()
|
|
41
|
-
return <KpiCard label={t('courses.kpi.fees') || 'Platform fees'} icon="Percent" loading={!s} value={s ? formatMoney(s.platformFeeTotal, s.currency) : undefined} />
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
/** Commerce KPIs are contributed only for the modules the host opted into —
|
|
45
|
-
* a host embedding courses as a lightweight members feature must not get
|
|
46
|
-
* revenue/MRR cards broadcast onto its home dashboard (FAY-1247 rule). */
|
|
47
|
-
export function createCoursesDashboardWidgets(
|
|
48
|
-
modules: { sales?: boolean; subscriptions?: boolean; financial?: boolean } = {},
|
|
49
|
-
): DashboardWidgetDef[] {
|
|
50
|
-
return [
|
|
51
|
-
...(modules.sales
|
|
52
|
-
? [
|
|
53
|
-
defineKpiWidget({ id: 'courses.kpi.revenue', title: 'courses.kpi.revenue', domain: 'courses', defaultOrder: 0, component: RevenueKpi }),
|
|
54
|
-
defineKpiWidget({ id: 'courses.kpi.sales', title: 'courses.kpi.sales', domain: 'courses', defaultOrder: 1, component: SalesKpi }),
|
|
55
|
-
]
|
|
56
|
-
: []),
|
|
57
|
-
...(modules.subscriptions
|
|
58
|
-
? [defineKpiWidget({ id: 'courses.kpi.mrr', title: 'courses.kpi.mrr', domain: 'courses', defaultOrder: 2, component: MrrKpi })]
|
|
59
|
-
: []),
|
|
60
|
-
...(modules.financial
|
|
61
|
-
? [defineKpiWidget({ id: 'courses.kpi.fees', title: 'courses.kpi.fees', domain: 'courses', defaultOrder: 3, component: FeesKpi })]
|
|
62
|
-
: []),
|
|
63
|
-
]
|
|
64
|
-
}
|
package/src/index.ts
DELETED
|
@@ -1,150 +0,0 @@
|
|
|
1
|
-
import * as React from 'react'
|
|
2
|
-
import type { PluginManifest, PluginScope, VerticalId } from '@fayz-ai/core'
|
|
3
|
-
import { registerTranslations, registerPluginFactory } from '@fayz-ai/core'
|
|
4
|
-
import {
|
|
5
|
-
getCoursesProviderOptional,
|
|
6
|
-
setCoursesProvider,
|
|
7
|
-
createMockCoursesProvider,
|
|
8
|
-
setCoursesTenantResolver,
|
|
9
|
-
} from '@fayz-ai/courses'
|
|
10
|
-
import { useOrganizationStore } from '@fayz-ai/saas'
|
|
11
|
-
import { coursesLocales } from './locales'
|
|
12
|
-
import { CoursesListPage } from './pages/CoursesListPage'
|
|
13
|
-
import { CourseEditorPage } from './pages/CourseEditorPage'
|
|
14
|
-
import { SalesPage } from './pages/SalesPage'
|
|
15
|
-
import { CourseOrderDetailView } from './views/CourseOrderDetailView'
|
|
16
|
-
import { SubscriptionsPage } from './pages/SubscriptionsPage'
|
|
17
|
-
import { FinancialPage } from './pages/FinancialPage'
|
|
18
|
-
import { ReportsPage } from './pages/ReportsPage'
|
|
19
|
-
import { CoursesSettingsTab } from './settings/CoursesSettings'
|
|
20
|
-
import { createCoursesDashboardWidgets } from './dashboardWidgets'
|
|
21
|
-
import { createStripeConnector } from './connectors/stripe'
|
|
22
|
-
|
|
23
|
-
// ---------------------------------------------------------------------------
|
|
24
|
-
// @fayz-ai/plugin-courses — admin management for the courses domain. This is a
|
|
25
|
-
// PLUGIN, not an app: it only contributes nav + routes + settings + dashboard
|
|
26
|
-
// widgets to the @fayz-ai/saas admin scaffold. Login, auth, permissions, the
|
|
27
|
-
// sidebar/AppShell chrome and layout all come from @fayz-ai/saas — the plugin
|
|
28
|
-
// never rebuilds them. Its screens render inside the saas AdminShell and use
|
|
29
|
-
// @fayz-ai/ui primitives.
|
|
30
|
-
// ---------------------------------------------------------------------------
|
|
31
|
-
|
|
32
|
-
export interface CoursesPluginOptions {
|
|
33
|
-
navPosition?: number
|
|
34
|
-
navSection?: 'main' | 'secondary' | 'settings'
|
|
35
|
-
navLabel?: string
|
|
36
|
-
/** Nav labels for the module entries (default English). */
|
|
37
|
-
labels?: {
|
|
38
|
-
membersArea?: string
|
|
39
|
-
sales?: string
|
|
40
|
-
subscriptions?: string
|
|
41
|
-
financial?: string
|
|
42
|
-
reports?: string
|
|
43
|
-
}
|
|
44
|
-
scope?: PluginScope
|
|
45
|
-
verticalId?: VerticalId
|
|
46
|
-
/**
|
|
47
|
-
* Commerce-vertical modules — ALL OFF by default (the one architectural
|
|
48
|
-
* rule: shared-surface contributions are opt-in, never broadcast — FAY-1247).
|
|
49
|
-
* course-admin (the vertical owner) enables everything via its PluginRef
|
|
50
|
-
* config; a host embedding courses as a lightweight members feature (e.g.
|
|
51
|
-
* agency-os's "Memberships") gets only the base nav entry + editor.
|
|
52
|
-
*/
|
|
53
|
-
modules?: {
|
|
54
|
-
membersArea?: boolean
|
|
55
|
-
sales?: boolean
|
|
56
|
-
subscriptions?: boolean
|
|
57
|
-
financial?: boolean
|
|
58
|
-
reports?: boolean
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
const asComp = (c: unknown) => c as React.ComponentType<unknown>
|
|
63
|
-
const read = (feature: string) => ({ feature, action: 'read' as const })
|
|
64
|
-
|
|
65
|
-
export function createCoursesPlugin(options?: CoursesPluginOptions): PluginManifest {
|
|
66
|
-
// Register locales globally so keys resolve even before the I18nProvider mounts.
|
|
67
|
-
registerTranslations(coursesLocales)
|
|
68
|
-
|
|
69
|
-
// Scope Supabase queries to the active organization (the creator/tenant). The
|
|
70
|
-
// mock provider ignores this; the Supabase provider appends .eq('tenant_id').
|
|
71
|
-
setCoursesTenantResolver(() => useOrganizationStore.getState().currentOrg?.id)
|
|
72
|
-
|
|
73
|
-
// Defensive default: if the host app didn't set a provider, fall back to a
|
|
74
|
-
// mock so the admin still renders. Apps that share admin↔member set their own.
|
|
75
|
-
if (!getCoursesProviderOptional()) {
|
|
76
|
-
setCoursesProvider(createMockCoursesProvider())
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
const base = options?.navPosition ?? 1
|
|
80
|
-
const section = options?.navSection ?? 'main'
|
|
81
|
-
const modules = options?.modules ?? {}
|
|
82
|
-
|
|
83
|
-
// Opt-in nav/routes per module. Position offsets stay stable relative to the
|
|
84
|
-
// enabled set so hosts control exactly where the block sits.
|
|
85
|
-
const labels = options?.labels ?? {}
|
|
86
|
-
const navigation = [
|
|
87
|
-
{ section, position: base, label: options?.navLabel ?? 'Courses', route: '/courses', icon: 'BookOpen', permission: read('courses') },
|
|
88
|
-
// The members area is no longer a standalone nav entry — it lives inside each
|
|
89
|
-
// course's record (CourseEditorPage → "Alunos" tab). modules.membersArea is
|
|
90
|
-
// kept accepted-but-inert for backward compat with existing manifests.
|
|
91
|
-
...(modules.sales ? [{ section, position: base + 2, label: labels.sales ?? 'Sales', route: '/courses/sales', icon: 'TrendingUp', permission: read('courses.sales') }] : []),
|
|
92
|
-
...(modules.subscriptions ? [{ section, position: base + 3, label: labels.subscriptions ?? 'Subscriptions', route: '/courses/subscriptions', icon: 'Repeat', permission: read('courses.sales') }] : []),
|
|
93
|
-
...(modules.financial ? [{ section, position: base + 4, label: labels.financial ?? 'Financial', route: '/courses/financial', icon: 'CreditCard', permission: read('courses.finance') }] : []),
|
|
94
|
-
...(modules.reports ? [{ section, position: base + 5, label: labels.reports ?? 'Reports', route: '/courses/reports', icon: 'BarChart3', permission: read('courses.sales') }] : []),
|
|
95
|
-
]
|
|
96
|
-
|
|
97
|
-
const routes = [
|
|
98
|
-
// Static segments outscore the `:id` param in the saas router, so these
|
|
99
|
-
// resolve before the editor route (AdminShell sorts by routeScore).
|
|
100
|
-
{ path: '/courses', component: asComp(CoursesListPage), permission: read('courses') },
|
|
101
|
-
...(modules.sales ? [{ path: '/courses/sales', component: asComp(SalesPage), permission: read('courses.sales') }] : []),
|
|
102
|
-
// Order detail — static `/courses/sales/:id` outscores `/courses/:id`.
|
|
103
|
-
...(modules.sales ? [{ path: '/courses/sales/:id', component: asComp(CourseOrderDetailView), permission: read('courses.sales') }] : []),
|
|
104
|
-
...(modules.subscriptions ? [{ path: '/courses/subscriptions', component: asComp(SubscriptionsPage), permission: read('courses.sales') }] : []),
|
|
105
|
-
...(modules.financial ? [{ path: '/courses/financial', component: asComp(FinancialPage), permission: read('courses.finance') }] : []),
|
|
106
|
-
...(modules.reports ? [{ path: '/courses/reports', component: asComp(ReportsPage), permission: read('courses.sales') }] : []),
|
|
107
|
-
{ path: '/courses/:id', component: asComp(CourseEditorPage) },
|
|
108
|
-
]
|
|
109
|
-
|
|
110
|
-
return {
|
|
111
|
-
id: 'courses',
|
|
112
|
-
name: options?.navLabel ?? 'Courses',
|
|
113
|
-
icon: 'BookOpen',
|
|
114
|
-
version: '1.0.0',
|
|
115
|
-
scope: options?.scope ?? 'vertical',
|
|
116
|
-
verticalId: options?.verticalId,
|
|
117
|
-
// LMS is its own business model with opt-in commerce modules
|
|
118
|
-
// (members/sales/subscriptions — see modules.* below) — targets the
|
|
119
|
-
// saas/admin Panel world rather than the ecommerce storefront.
|
|
120
|
-
scaffolds: ['saas'],
|
|
121
|
-
defaultEnabled: true,
|
|
122
|
-
dependencies: [],
|
|
123
|
-
declaredFeatures: [
|
|
124
|
-
{ id: 'courses', label: 'Courses', group: 'Courses' },
|
|
125
|
-
{ id: 'courses.sales', label: 'Sales', group: 'Courses' },
|
|
126
|
-
{ id: 'courses.finance', label: 'Financial', group: 'Courses' },
|
|
127
|
-
],
|
|
128
|
-
declaredLimits: [
|
|
129
|
-
{ key: 'courses', label: 'Courses', table: 'plg_courses_courses' },
|
|
130
|
-
],
|
|
131
|
-
navigation,
|
|
132
|
-
routes,
|
|
133
|
-
widgets: [],
|
|
134
|
-
// Commerce KPIs on the shared 'home' surface follow the same opt-in rule:
|
|
135
|
-
// no module enabled → no widgets broadcast into a host's dashboard.
|
|
136
|
-
dashboardWidgets: createCoursesDashboardWidgets(modules),
|
|
137
|
-
connectors: [createStripeConnector()],
|
|
138
|
-
settings: [
|
|
139
|
-
{ id: 'courses', label: 'Courses', icon: 'BookOpen', component: asComp(CoursesSettingsTab), order: 20, permission: read('courses') },
|
|
140
|
-
],
|
|
141
|
-
locales: coursesLocales,
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
// Self-register the factory so renderApp(manifest) with a PluginRef { id:
|
|
146
|
-
// 'courses' } resolves without a separate plugins.generated.ts entry (apps may
|
|
147
|
-
// still register explicitly; registration is idempotent on id).
|
|
148
|
-
registerPluginFactory('courses', createCoursesPlugin as (config?: Record<string, unknown>) => PluginManifest)
|
|
149
|
-
|
|
150
|
-
export { coursesLocales } from './locales'
|
package/src/lib/format.ts
DELETED
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
// Small display helpers shared across the courses admin pages. Kept local so the
|
|
2
|
-
// plugin depends only on @fayz-ai/core + @fayz-ai/ui + @fayz-ai/courses.
|
|
3
|
-
|
|
4
|
-
export function formatMoney(value: number, currency = 'BRL'): string {
|
|
5
|
-
try {
|
|
6
|
-
return new Intl.NumberFormat('pt-BR', { style: 'currency', currency }).format(value)
|
|
7
|
-
} catch {
|
|
8
|
-
return `${currency} ${value.toFixed(2)}`
|
|
9
|
-
}
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
export function formatDate(iso: string): string {
|
|
13
|
-
try {
|
|
14
|
-
return new Intl.DateTimeFormat('pt-BR', { day: '2-digit', month: '2-digit', year: 'numeric' }).format(new Date(iso))
|
|
15
|
-
} catch {
|
|
16
|
-
return iso.slice(0, 10)
|
|
17
|
-
}
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export function formatPercent(fraction: number, digits = 0): string {
|
|
21
|
-
return `${(fraction * 100).toFixed(digits)}%`
|
|
22
|
-
}
|