@cdx-di-template/webtemplate-investment-widget 0.0.1
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 +40 -0
- package/dist/.babelrc.template +12 -0
- package/dist/.eslintrc.json.template +18 -0
- package/dist/jest.config.ts.template +12 -0
- package/dist/module-federation.config.ts.template +11 -0
- package/dist/project.json.template +77 -0
- package/dist/src/__mocks__/investmentMock.json +138 -0
- package/dist/src/app/__name__.tsx.template +447 -0
- package/dist/src/app/components/types/branding.ts.template +51 -0
- package/dist/src/assets/.gitkeep +0 -0
- package/dist/src/bootstrap.tsx.template +18 -0
- package/dist/src/environments/environment.prod.ts.template +5 -0
- package/dist/src/environments/environment.qal.ts.template +4 -0
- package/dist/src/environments/environment.ts.template +7 -0
- package/dist/src/index.html.template +12 -0
- package/dist/src/locales/en-US/translation.json.template +22 -0
- package/dist/src/locales/es-ES/translation.json.template +22 -0
- package/dist/src/locales/fr-CA/translation.json.template +22 -0
- package/dist/src/locales/index.ts.template +11 -0
- package/dist/src/locales/zh-TW/translation.json.template +22 -0
- package/dist/src/main.ts.template +1 -0
- package/dist/src/setupTests.ts.template +17 -0
- package/dist/src/types.ts.template +26 -0
- package/dist/tsconfig.app.json.template +24 -0
- package/dist/tsconfig.json.template +21 -0
- package/dist/tsconfig.spec.json.template +25 -0
- package/dist/webpack.config.ts.template +21 -0
- package/package.json +16 -0
package/README.md
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# @cdx-extensions/webtemplate-investment-widget
|
|
2
|
+
|
|
3
|
+
Nx generator template files for a CDX investment portfolio web widget, in the same format as `@cdx-extensions/widget-template-web` `appGenerator/files`.
|
|
4
|
+
|
|
5
|
+
## Contents
|
|
6
|
+
|
|
7
|
+
- `module-federation.config.ts.template`
|
|
8
|
+
- `src/**/*.template` — widget source with EJS placeholders (`<%= name %>`, `<%= upperCamel(name) %>`, `__name__`)
|
|
9
|
+
- `src/__mocks__/investmentMock.json` — static mock portfolio data (no `.template` suffix)
|
|
10
|
+
|
|
11
|
+
## Build
|
|
12
|
+
|
|
13
|
+
From the repo root:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npx nx build webtemplate-investment-widget
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Or from this directory:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
npm run build
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Output is written to `dist/` (local) and `dist/packages/webtemplate-investment-widget/` (staged publish layout).
|
|
26
|
+
|
|
27
|
+
Published `dist/` includes:
|
|
28
|
+
|
|
29
|
+
- `module-federation.config.ts.template`
|
|
30
|
+
- `src/**/*.template` — widget source with EJS placeholders
|
|
31
|
+
- Project scaffold templates (`.babelrc`, `project.json`, `tsconfig.*`, `jest.config.ts`, `webpack.config.ts`, etc.) shared from `tools/widget-di-template-generator/projectFiles/`
|
|
32
|
+
- `src/__mocks__/investmentMock.json` — static mock portfolio data
|
|
33
|
+
|
|
34
|
+
## Publish
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
npx nx publish-npm webtemplate-investment-widget
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Ensure you are authenticated to the Candescent npm registry before publishing.
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": ["plugin:@nx/react", "<%= relativeToRoot %>.eslintrc.json"],
|
|
3
|
+
"ignorePatterns": ["!**/*"],
|
|
4
|
+
"overrides": [
|
|
5
|
+
{
|
|
6
|
+
"files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
|
|
7
|
+
"rules": {}
|
|
8
|
+
},
|
|
9
|
+
{
|
|
10
|
+
"files": ["*.ts", "*.tsx"],
|
|
11
|
+
"rules": {}
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
"files": ["*.js", "*.jsx"],
|
|
15
|
+
"rules": {}
|
|
16
|
+
}
|
|
17
|
+
]
|
|
18
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/* eslint-disable */
|
|
2
|
+
export default {
|
|
3
|
+
displayName: '<%= name %>',
|
|
4
|
+
preset: '<%= relativeToRoot %>jest.preset.js',
|
|
5
|
+
transform: {
|
|
6
|
+
'^(?!.*\\.(js|jsx|ts|tsx|css|json)$)': '@nx/react/plugins/jest',
|
|
7
|
+
'^.+\\.[tj]sx?$': ['babel-jest', { presets: ['@nx/react/babel'] }],
|
|
8
|
+
},
|
|
9
|
+
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'],
|
|
10
|
+
coverageDirectory: '<%= relativeToRoot %>coverage/<%= projectRoot %>',
|
|
11
|
+
setupFilesAfterEnv: ['<rootDir>/src/setupTests.ts'],
|
|
12
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { ModuleFederationConfig } from '@nx/webpack';
|
|
2
|
+
|
|
3
|
+
const config: ModuleFederationConfig = {
|
|
4
|
+
disableNxRuntimeLibraryControlPlugin: true,
|
|
5
|
+
name: '<%= name %>',
|
|
6
|
+
exposes: {
|
|
7
|
+
'./<%= upperCamel(name) %>': './src/app/<%= upperCamel(name) %>',
|
|
8
|
+
},
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export default config;
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "<%= name %>",
|
|
3
|
+
"$schema": "<%= relativeToRoot %>node_modules/nx/schemas/project-schema.json",
|
|
4
|
+
"root": "<%= projectRoot %>",
|
|
5
|
+
"sourceRoot": "<%= projectRoot %>/src",
|
|
6
|
+
"projectType": "application",
|
|
7
|
+
"tags": [],
|
|
8
|
+
"targets": {
|
|
9
|
+
"build": {
|
|
10
|
+
"executor": "@nx/webpack:webpack",
|
|
11
|
+
"outputs": ["{options.outputPath}"],
|
|
12
|
+
"defaultConfiguration": "qal",
|
|
13
|
+
"options": {
|
|
14
|
+
"compiler": "babel",
|
|
15
|
+
"outputPath": "dist/<%= projectRoot %>",
|
|
16
|
+
"index": "<%= projectRoot %>/src/index.html",
|
|
17
|
+
"baseHref": "/",
|
|
18
|
+
"main": "<%= projectRoot %>/src/main.ts",
|
|
19
|
+
"tsConfig": "<%= projectRoot %>/tsconfig.app.json",
|
|
20
|
+
"assets": [
|
|
21
|
+
"<%= projectRoot %>/src/favicon.ico",
|
|
22
|
+
"<%= projectRoot %>/src/assets",
|
|
23
|
+
"<%= projectRoot %>/src/locales"
|
|
24
|
+
],
|
|
25
|
+
"styles": [],
|
|
26
|
+
"scripts": [],
|
|
27
|
+
"webpackConfig": "<%= projectRoot %>/webpack.config.ts"
|
|
28
|
+
},
|
|
29
|
+
"configurations": <%- buildConfigurations %>
|
|
30
|
+
},
|
|
31
|
+
"serve": {
|
|
32
|
+
"executor": "@nx/react:module-federation-dev-server",
|
|
33
|
+
"defaultConfiguration": "development",
|
|
34
|
+
"options": {
|
|
35
|
+
"buildTarget": "<%= name %>:build",
|
|
36
|
+
"hmr": true,
|
|
37
|
+
"port": <%= port %>
|
|
38
|
+
},
|
|
39
|
+
"configurations": {
|
|
40
|
+
"development": {
|
|
41
|
+
"buildTarget": "<%= name %>:build:development"
|
|
42
|
+
},
|
|
43
|
+
"production": {
|
|
44
|
+
"buildTarget": "<%= name %>:build:production",
|
|
45
|
+
"hmr": false
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
"lint": {
|
|
50
|
+
"executor": "@nx/eslint:lint"
|
|
51
|
+
},
|
|
52
|
+
"serve-static": {
|
|
53
|
+
"executor": "@nx/web:file-server",
|
|
54
|
+
"defaultConfiguration": "production",
|
|
55
|
+
"options": {
|
|
56
|
+
"buildTarget": "<%= name %>:build",
|
|
57
|
+
"watch": false,
|
|
58
|
+
"port": <%= port %>
|
|
59
|
+
},
|
|
60
|
+
"configurations": {
|
|
61
|
+
"development": {
|
|
62
|
+
"buildTarget": "<%= name %>:build:development"
|
|
63
|
+
},
|
|
64
|
+
"production": {
|
|
65
|
+
"buildTarget": "<%= name %>:build:production"
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
"test": {
|
|
70
|
+
"executor": "@nx/jest:jest",
|
|
71
|
+
"outputs": ["{workspaceRoot}/coverage/{projectRoot}"],
|
|
72
|
+
"options": {
|
|
73
|
+
"jestConfig": "<%= projectRoot %>/jest.config.ts"
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
{
|
|
2
|
+
"totalValue": 175430.5,
|
|
3
|
+
"totalValueChange": 3240.75,
|
|
4
|
+
"totalValueChangePercent": 2.65,
|
|
5
|
+
"cashBalance": 15230.0,
|
|
6
|
+
"investedValue": 110200.5,
|
|
7
|
+
"dayChange": 1250.3,
|
|
8
|
+
"dayChangePercent": 1.01,
|
|
9
|
+
"holdings": [
|
|
10
|
+
{
|
|
11
|
+
"symbol": "AAPL",
|
|
12
|
+
"name": "Apple Inc.",
|
|
13
|
+
"shares": 45,
|
|
14
|
+
"currentPrice": 175.23,
|
|
15
|
+
"value": 7885.35,
|
|
16
|
+
"change": 125.5,
|
|
17
|
+
"changePercent": 1.62,
|
|
18
|
+
"allocation": 6.29
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
"symbol": "MSFT",
|
|
22
|
+
"name": "Microsoft Corporation",
|
|
23
|
+
"shares": 30,
|
|
24
|
+
"currentPrice": 378.85,
|
|
25
|
+
"value": 11365.5,
|
|
26
|
+
"change": 89.25,
|
|
27
|
+
"changePercent": 0.79,
|
|
28
|
+
"allocation": 9.07
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
"symbol": "GOOGL",
|
|
32
|
+
"name": "Alphabet Inc.",
|
|
33
|
+
"shares": 25,
|
|
34
|
+
"currentPrice": 142.56,
|
|
35
|
+
"value": 3564.0,
|
|
36
|
+
"change": -45.2,
|
|
37
|
+
"changePercent": -1.25,
|
|
38
|
+
"allocation": 2.84
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
"symbol": "AMZN",
|
|
42
|
+
"name": "Amazon.com Inc.",
|
|
43
|
+
"shares": 40,
|
|
44
|
+
"currentPrice": 145.32,
|
|
45
|
+
"value": 5812.8,
|
|
46
|
+
"change": 78.4,
|
|
47
|
+
"changePercent": 1.37,
|
|
48
|
+
"allocation": 4.64
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
"symbol": "TSLA",
|
|
52
|
+
"name": "Tesla, Inc.",
|
|
53
|
+
"shares": 50,
|
|
54
|
+
"currentPrice": 248.5,
|
|
55
|
+
"value": 12425.0,
|
|
56
|
+
"change": 325.0,
|
|
57
|
+
"changePercent": 2.68,
|
|
58
|
+
"allocation": 9.91
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
"symbol": "SPY",
|
|
62
|
+
"name": "SPDR S&P 500 ETF",
|
|
63
|
+
"shares": 100,
|
|
64
|
+
"currentPrice": 445.67,
|
|
65
|
+
"value": 44567.0,
|
|
66
|
+
"change": 567.0,
|
|
67
|
+
"changePercent": 1.29,
|
|
68
|
+
"allocation": 35.52
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
"symbol": "VTI",
|
|
72
|
+
"name": "Vanguard Total Stock Market ETF",
|
|
73
|
+
"shares": 80,
|
|
74
|
+
"currentPrice": 234.12,
|
|
75
|
+
"value": 18729.6,
|
|
76
|
+
"change": 234.4,
|
|
77
|
+
"changePercent": 1.27,
|
|
78
|
+
"allocation": 14.93
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
"symbol": "BND",
|
|
82
|
+
"name": "Vanguard Total Bond Market ETF",
|
|
83
|
+
"shares": 120,
|
|
84
|
+
"currentPrice": 78.45,
|
|
85
|
+
"value": 9414.0,
|
|
86
|
+
"change": 12.0,
|
|
87
|
+
"changePercent": 0.13,
|
|
88
|
+
"allocation": 7.51
|
|
89
|
+
}
|
|
90
|
+
],
|
|
91
|
+
"recentActivity": [
|
|
92
|
+
{
|
|
93
|
+
"id": "1",
|
|
94
|
+
"type": "BUY",
|
|
95
|
+
"symbol": "AAPL",
|
|
96
|
+
"shares": 5,
|
|
97
|
+
"price": 174.5,
|
|
98
|
+
"date": "2024-01-15",
|
|
99
|
+
"time": "10:30 AM"
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
"id": "2",
|
|
103
|
+
"type": "SELL",
|
|
104
|
+
"symbol": "GOOGL",
|
|
105
|
+
"shares": 10,
|
|
106
|
+
"price": 143.2,
|
|
107
|
+
"date": "2024-01-14",
|
|
108
|
+
"time": "2:15 PM"
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
"id": "3",
|
|
112
|
+
"type": "DIVIDEND",
|
|
113
|
+
"symbol": "MSFT",
|
|
114
|
+
"amount": 45.0,
|
|
115
|
+
"date": "2024-01-10",
|
|
116
|
+
"time": "9:00 AM"
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
"id": "4",
|
|
120
|
+
"type": "BUY",
|
|
121
|
+
"symbol": "SPY",
|
|
122
|
+
"shares": 20,
|
|
123
|
+
"price": 444.0,
|
|
124
|
+
"date": "2024-01-08",
|
|
125
|
+
"time": "11:45 AM"
|
|
126
|
+
}
|
|
127
|
+
],
|
|
128
|
+
"performance": {
|
|
129
|
+
"oneDay": 1.01,
|
|
130
|
+
"oneWeek": 2.35,
|
|
131
|
+
"oneMonth": 5.42,
|
|
132
|
+
"threeMonths": 8.67,
|
|
133
|
+
"sixMonths": 12.34,
|
|
134
|
+
"oneYear": 18.56,
|
|
135
|
+
"allTime": 24.89
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
@@ -0,0 +1,447 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import { ThemeProvider, CssBaseline, useTheme } from '@mui/material';
|
|
3
|
+
import { createTheme, type Theme } from '@mui/material/styles';
|
|
4
|
+
import { PlatformSDK } from '@cdx-extensions/di-sdk';
|
|
5
|
+
import { environment } from '../environments/environment';
|
|
6
|
+
import investment from '../__mocks__/investmentMock.json';
|
|
7
|
+
import type { <%= upperCamel(name) %>Props } from '../types';
|
|
8
|
+
import { defaultBranding, type BrandingConfig } from './components/types/branding';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Federated module entry point component. This will be the component that is used
|
|
12
|
+
* when displayed as an MFE on a page.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const DEFAULT_BRANDING_ID = 'branding-1';
|
|
16
|
+
|
|
17
|
+
/** Fixed allocation colors for the donut chart (same as pre-theme sample). */
|
|
18
|
+
const DONUT_CHART_COLORS = [
|
|
19
|
+
'#1A6CDA',
|
|
20
|
+
'#10B981',
|
|
21
|
+
'#F59E0B',
|
|
22
|
+
'#EF4444',
|
|
23
|
+
'#8B5CF6',
|
|
24
|
+
'#EC4899',
|
|
25
|
+
'#06B6D4',
|
|
26
|
+
'#84CC16',
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
/** Build MUI theme from SDK theme; fallback to minimal theme. */
|
|
30
|
+
function resolveTheme(sdkTheme: unknown): Theme {
|
|
31
|
+
if (sdkTheme && typeof sdkTheme === 'object' && Object.keys(sdkTheme as object).length > 0) {
|
|
32
|
+
return createTheme(sdkTheme as object);
|
|
33
|
+
}
|
|
34
|
+
return createTheme({});
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Map MUI theme to BrandingConfig for inline UI. */
|
|
38
|
+
function themeToBrandingConfig(theme: Theme): BrandingConfig {
|
|
39
|
+
const p = theme.palette;
|
|
40
|
+
const t = theme.typography;
|
|
41
|
+
return {
|
|
42
|
+
colors: {
|
|
43
|
+
primary: p.primary?.main ?? defaultBranding.colors.primary,
|
|
44
|
+
secondary: p.secondary?.main ?? defaultBranding.colors.secondary,
|
|
45
|
+
background: p.background?.default ?? defaultBranding.colors.background,
|
|
46
|
+
surface: p.background?.paper ?? defaultBranding.colors.surface,
|
|
47
|
+
text: p.text?.primary ?? defaultBranding.colors.text,
|
|
48
|
+
textSecondary: p.text?.secondary ?? defaultBranding.colors.textSecondary,
|
|
49
|
+
error: p.error?.main ?? defaultBranding.colors.error,
|
|
50
|
+
warning: p.warning?.main ?? defaultBranding.colors.warning,
|
|
51
|
+
success: p.success?.main ?? defaultBranding.colors.success,
|
|
52
|
+
},
|
|
53
|
+
fonts: {
|
|
54
|
+
primary: t.fontFamily ?? defaultBranding.fonts.primary,
|
|
55
|
+
secondary: t.fontFamily ?? defaultBranding.fonts.secondary,
|
|
56
|
+
},
|
|
57
|
+
spacing: defaultBranding.spacing,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Font metrics from the active MUI theme (host when embedded, SDK when standalone). */
|
|
62
|
+
function typographyVariant(
|
|
63
|
+
theme: Theme,
|
|
64
|
+
key: 'caption' | 'body2' | 'subtitle2' | 'h6',
|
|
65
|
+
): React.CSSProperties {
|
|
66
|
+
const v = theme.typography[key];
|
|
67
|
+
if (v && typeof v === 'object' && !Array.isArray(v)) {
|
|
68
|
+
return v as React.CSSProperties;
|
|
69
|
+
}
|
|
70
|
+
return {};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function getPortfolioStyles(
|
|
74
|
+
branding: BrandingConfig,
|
|
75
|
+
muiTheme: Theme,
|
|
76
|
+
): { [key: string]: React.CSSProperties } {
|
|
77
|
+
const divider = muiTheme.palette.divider;
|
|
78
|
+
const t = muiTheme.typography;
|
|
79
|
+
const borderRadius = Number.parseFloat(String(muiTheme.shape.borderRadius));
|
|
80
|
+
return {
|
|
81
|
+
container: {
|
|
82
|
+
backgroundColor: branding.colors.background,
|
|
83
|
+
fontFamily: branding.fonts.primary,
|
|
84
|
+
padding: `${branding.spacing.medium}px`,
|
|
85
|
+
borderRadius: '8px',
|
|
86
|
+
border: `1px solid ${divider}`,
|
|
87
|
+
boxShadow: muiTheme.shadows[1],
|
|
88
|
+
},
|
|
89
|
+
subtitle: {
|
|
90
|
+
...typographyVariant(muiTheme, 'caption'),
|
|
91
|
+
color: branding.colors.textSecondary,
|
|
92
|
+
margin: 0,
|
|
93
|
+
},
|
|
94
|
+
holdingsCard: {
|
|
95
|
+
backgroundColor: 'transparent',
|
|
96
|
+
borderRadius: '0',
|
|
97
|
+
padding: '0',
|
|
98
|
+
overflowX: 'auto',
|
|
99
|
+
},
|
|
100
|
+
viewModeButton: {
|
|
101
|
+
...typographyVariant(muiTheme, 'body2'),
|
|
102
|
+
fontWeight: t.fontWeightMedium,
|
|
103
|
+
padding: '8px 16px',
|
|
104
|
+
borderRadius: `${Number.isNaN(borderRadius) ? 4 : borderRadius}px`,
|
|
105
|
+
border: 'none',
|
|
106
|
+
cursor: 'pointer',
|
|
107
|
+
transition: 'all 0.2s',
|
|
108
|
+
},
|
|
109
|
+
viewModeButtonActive: {
|
|
110
|
+
fontWeight: t.fontWeightBold,
|
|
111
|
+
},
|
|
112
|
+
chartsContainer: {
|
|
113
|
+
display: 'grid',
|
|
114
|
+
gridTemplateColumns: 'repeat(auto-fit, minmax(280px, 1fr))',
|
|
115
|
+
gap: `${branding.spacing.medium}px`,
|
|
116
|
+
},
|
|
117
|
+
chartSection: {
|
|
118
|
+
backgroundColor: branding.colors.surface,
|
|
119
|
+
borderRadius: '6px',
|
|
120
|
+
padding: `${branding.spacing.medium}px`,
|
|
121
|
+
border: `1px solid ${divider}`,
|
|
122
|
+
},
|
|
123
|
+
chartTitle: {
|
|
124
|
+
...typographyVariant(muiTheme, 'subtitle2'),
|
|
125
|
+
color: branding.colors.text,
|
|
126
|
+
margin: 0,
|
|
127
|
+
marginBottom: muiTheme.spacing(1.5),
|
|
128
|
+
},
|
|
129
|
+
donutChartContainer: {
|
|
130
|
+
display: 'flex',
|
|
131
|
+
flexDirection: 'column',
|
|
132
|
+
alignItems: 'center',
|
|
133
|
+
marginBottom: `${branding.spacing.medium}px`,
|
|
134
|
+
},
|
|
135
|
+
donutChart: {
|
|
136
|
+
margin: '0 auto',
|
|
137
|
+
width: '200px',
|
|
138
|
+
height: '200px',
|
|
139
|
+
},
|
|
140
|
+
donutCenterText: {
|
|
141
|
+
...typographyVariant(muiTheme, 'body2'),
|
|
142
|
+
fontWeight: t.fontWeightMedium,
|
|
143
|
+
color: branding.colors.textSecondary,
|
|
144
|
+
fill: branding.colors.textSecondary,
|
|
145
|
+
},
|
|
146
|
+
donutCenterValue: {
|
|
147
|
+
...typographyVariant(muiTheme, 'h6'),
|
|
148
|
+
color: branding.colors.text,
|
|
149
|
+
fill: branding.colors.text,
|
|
150
|
+
},
|
|
151
|
+
legendContainer: {
|
|
152
|
+
display: 'grid',
|
|
153
|
+
gridTemplateColumns: 'repeat(2, 1fr)',
|
|
154
|
+
gap: '12px',
|
|
155
|
+
width: '100%',
|
|
156
|
+
},
|
|
157
|
+
legendItem: {
|
|
158
|
+
display: 'flex',
|
|
159
|
+
alignItems: 'center',
|
|
160
|
+
gap: '8px',
|
|
161
|
+
},
|
|
162
|
+
legendColor: {
|
|
163
|
+
width: '12px',
|
|
164
|
+
height: '12px',
|
|
165
|
+
borderRadius: '2px',
|
|
166
|
+
flexShrink: 0,
|
|
167
|
+
},
|
|
168
|
+
legendText: {
|
|
169
|
+
display: 'flex',
|
|
170
|
+
flexDirection: 'column',
|
|
171
|
+
gap: '2px',
|
|
172
|
+
},
|
|
173
|
+
legendSymbol: {
|
|
174
|
+
...typographyVariant(muiTheme, 'subtitle2'),
|
|
175
|
+
color: branding.colors.text,
|
|
176
|
+
},
|
|
177
|
+
legendAllocation: {
|
|
178
|
+
...typographyVariant(muiTheme, 'caption'),
|
|
179
|
+
color: branding.colors.textSecondary,
|
|
180
|
+
},
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
interface PortfolioData {
|
|
185
|
+
totalValue: number;
|
|
186
|
+
totalValueChange: number;
|
|
187
|
+
totalValueChangePercent: number;
|
|
188
|
+
cashBalance: number;
|
|
189
|
+
investedValue: number;
|
|
190
|
+
dayChange: number;
|
|
191
|
+
dayChangePercent: number;
|
|
192
|
+
holdings: Holding[];
|
|
193
|
+
recentActivity: Activity[];
|
|
194
|
+
performance: PerformanceMetrics;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const MOCK_PORTFOLIO_DATA = investment as PortfolioData;
|
|
198
|
+
|
|
199
|
+
interface Holding {
|
|
200
|
+
symbol: string;
|
|
201
|
+
name: string;
|
|
202
|
+
shares: number;
|
|
203
|
+
currentPrice: number;
|
|
204
|
+
value: number;
|
|
205
|
+
change: number;
|
|
206
|
+
changePercent: number;
|
|
207
|
+
allocation: number;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
interface Activity {
|
|
211
|
+
id: string;
|
|
212
|
+
type: 'BUY' | 'SELL' | 'DIVIDEND' | 'TRANSFER';
|
|
213
|
+
symbol?: string;
|
|
214
|
+
shares?: number;
|
|
215
|
+
price?: number;
|
|
216
|
+
amount?: number;
|
|
217
|
+
date: string;
|
|
218
|
+
time: string;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
interface PerformanceMetrics {
|
|
222
|
+
oneDay: number;
|
|
223
|
+
oneWeek: number;
|
|
224
|
+
oneMonth: number;
|
|
225
|
+
threeMonths: number;
|
|
226
|
+
sixMonths: number;
|
|
227
|
+
oneYear: number;
|
|
228
|
+
allTime: number;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
interface PortfolioBodyProps extends <%= upperCamel(name) %>Props {
|
|
232
|
+
muiTheme: Theme;
|
|
233
|
+
userContextData: { fullName?: string } | undefined;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function PortfolioBody({ muiTheme, userContextData }: PortfolioBodyProps) {
|
|
237
|
+
const [portfolioData, setPortfolioData] = useState<PortfolioData>(MOCK_PORTFOLIO_DATA);
|
|
238
|
+
const [showHoldings] = useState(true);
|
|
239
|
+
const [viewMode] = useState<'chart' | 'list'>('chart');
|
|
240
|
+
|
|
241
|
+
const sdk = PlatformSDK.getInstance();
|
|
242
|
+
const branding = themeToBrandingConfig(muiTheme);
|
|
243
|
+
const styles = getPortfolioStyles(branding, muiTheme);
|
|
244
|
+
const chartColors = DONUT_CHART_COLORS;
|
|
245
|
+
const paperFill = muiTheme.palette.background.paper;
|
|
246
|
+
const primaryContrast = muiTheme.palette.primary.contrastText;
|
|
247
|
+
|
|
248
|
+
const baseUrl = environment.apiUrl || 'https://investmentmock.tiiny.site/investmentMock.json';
|
|
249
|
+
|
|
250
|
+
/*
|
|
251
|
+
* This function is used to trigger the refresh of the portfolio data
|
|
252
|
+
* It is used to refresh the portfolio data when the user clicks the refresh button
|
|
253
|
+
* For your development please remove this button and add some timer to refresh the portfolio data
|
|
254
|
+
* All external api call should be use this getHttpClient() method to make the api call
|
|
255
|
+
* @function triggerEffect
|
|
256
|
+
* This mockdata is used for only local usage please use the getHttpClient() method to make the api call
|
|
257
|
+
* @returns {void}
|
|
258
|
+
* To run this example in your local machine make this changes in the node_modules/@cdx-extensions/di-sdk-web/dist/httpClient.js
|
|
259
|
+
* file const isMock = false;
|
|
260
|
+
* Remove this line request.headers['Authorization'] = `Bearer ${access_token}`;
|
|
261
|
+
*/
|
|
262
|
+
const [triggerEffect, setTriggerEffect] = useState(false);
|
|
263
|
+
if (triggerEffect) {
|
|
264
|
+
sdk
|
|
265
|
+
.getHttpClient()
|
|
266
|
+
.get(baseUrl)
|
|
267
|
+
.then(response => {
|
|
268
|
+
setPortfolioData(response.data);
|
|
269
|
+
})
|
|
270
|
+
.catch(error => {
|
|
271
|
+
alert(error);
|
|
272
|
+
setPortfolioData(MOCK_PORTFOLIO_DATA);
|
|
273
|
+
});
|
|
274
|
+
setTriggerEffect(false);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const formatCurrency = (value: number): string => {
|
|
278
|
+
return new Intl.NumberFormat('en-US', {
|
|
279
|
+
style: 'currency',
|
|
280
|
+
currency: 'USD',
|
|
281
|
+
minimumFractionDigits: 2,
|
|
282
|
+
maximumFractionDigits: 2,
|
|
283
|
+
}).format(value);
|
|
284
|
+
};
|
|
285
|
+
|
|
286
|
+
const donutChartData = portfolioData.holdings.map((holding, index) => ({
|
|
287
|
+
...holding,
|
|
288
|
+
color: chartColors[index % chartColors.length],
|
|
289
|
+
angle: (holding.allocation / 100) * 360,
|
|
290
|
+
}));
|
|
291
|
+
|
|
292
|
+
let cumulativeAngle = -90;
|
|
293
|
+
const donutSegments = donutChartData.map(item => {
|
|
294
|
+
const startAngle = cumulativeAngle;
|
|
295
|
+
cumulativeAngle += item.angle;
|
|
296
|
+
return {
|
|
297
|
+
...item,
|
|
298
|
+
startAngle,
|
|
299
|
+
endAngle: cumulativeAngle,
|
|
300
|
+
};
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
const getCoordinates = (angle: number, radius: number, center: number) => {
|
|
304
|
+
const radians = (angle * Math.PI) / 180;
|
|
305
|
+
return {
|
|
306
|
+
x: center + radius * Math.cos(radians),
|
|
307
|
+
y: center + radius * Math.sin(radians),
|
|
308
|
+
};
|
|
309
|
+
};
|
|
310
|
+
|
|
311
|
+
const createDonutPath = (
|
|
312
|
+
startAngle: number,
|
|
313
|
+
endAngle: number,
|
|
314
|
+
innerRadius: number,
|
|
315
|
+
outerRadius: number,
|
|
316
|
+
center: number,
|
|
317
|
+
) => {
|
|
318
|
+
const startInner = getCoordinates(startAngle, innerRadius, center);
|
|
319
|
+
const startOuter = getCoordinates(startAngle, outerRadius, center);
|
|
320
|
+
const endInner = getCoordinates(endAngle, innerRadius, center);
|
|
321
|
+
const endOuter = getCoordinates(endAngle, outerRadius, center);
|
|
322
|
+
|
|
323
|
+
const largeArc = endAngle - startAngle > 180 ? 1 : 0;
|
|
324
|
+
|
|
325
|
+
return [
|
|
326
|
+
`M ${startOuter.x} ${startOuter.y}`,
|
|
327
|
+
`A ${outerRadius} ${outerRadius} 0 ${largeArc} 1 ${endOuter.x} ${endOuter.y}`,
|
|
328
|
+
`L ${endInner.x} ${endInner.y}`,
|
|
329
|
+
`A ${innerRadius} ${innerRadius} 0 ${largeArc} 0 ${startInner.x} ${startInner.y}`,
|
|
330
|
+
'Z',
|
|
331
|
+
].join(' ');
|
|
332
|
+
};
|
|
333
|
+
|
|
334
|
+
return (
|
|
335
|
+
<div style={styles.container}>
|
|
336
|
+
{showHoldings && (
|
|
337
|
+
<div style={styles.holdingsCard}>
|
|
338
|
+
{viewMode === 'chart' ? (
|
|
339
|
+
<div style={styles.chartsContainer}>
|
|
340
|
+
<div style={styles.chartSection}>
|
|
341
|
+
<h3 style={styles.chartTitle}>Portfolio Allocation</h3>
|
|
342
|
+
<p style={styles.subtitle}>Portfolio Overview</p>
|
|
343
|
+
<p style={styles.subtitle}>
|
|
344
|
+
Name : {userContextData?.fullName ?? 'N/A'}
|
|
345
|
+
</p>
|
|
346
|
+
<div style={styles.donutChartContainer}>
|
|
347
|
+
<svg width="200" height="200" style={styles.donutChart}>
|
|
348
|
+
<g transform="translate(100, 100)">
|
|
349
|
+
{donutSegments.map(segment => (
|
|
350
|
+
<g key={segment.symbol}>
|
|
351
|
+
<path
|
|
352
|
+
d={createDonutPath(segment.startAngle, segment.endAngle, 50, 70, 0)}
|
|
353
|
+
fill={segment.color}
|
|
354
|
+
stroke={paperFill}
|
|
355
|
+
strokeWidth="2"
|
|
356
|
+
style={{ cursor: 'pointer' }}
|
|
357
|
+
onMouseEnter={e => {
|
|
358
|
+
e.currentTarget.style.opacity = '0.8';
|
|
359
|
+
}}
|
|
360
|
+
onMouseLeave={e => {
|
|
361
|
+
e.currentTarget.style.opacity = '1';
|
|
362
|
+
}}
|
|
363
|
+
/>
|
|
364
|
+
</g>
|
|
365
|
+
))}
|
|
366
|
+
<circle cx="0" cy="0" r="50" fill={paperFill} />
|
|
367
|
+
<text x="0" y="-10" textAnchor="middle" style={styles.donutCenterText}>
|
|
368
|
+
Total
|
|
369
|
+
</text>
|
|
370
|
+
<text x="0" y="15" textAnchor="middle" style={styles.donutCenterValue}>
|
|
371
|
+
{formatCurrency(portfolioData.totalValue)}
|
|
372
|
+
</text>
|
|
373
|
+
</g>
|
|
374
|
+
</svg>
|
|
375
|
+
</div>
|
|
376
|
+
<div style={styles.legendContainer}>
|
|
377
|
+
{donutSegments.map(segment => (
|
|
378
|
+
<div key={segment.symbol} style={styles.legendItem}>
|
|
379
|
+
<div
|
|
380
|
+
style={{
|
|
381
|
+
...styles.legendColor,
|
|
382
|
+
backgroundColor: segment.color,
|
|
383
|
+
}}
|
|
384
|
+
/>
|
|
385
|
+
<div style={styles.legendText}>
|
|
386
|
+
<div style={styles.legendSymbol}>{segment.symbol}</div>
|
|
387
|
+
<div style={styles.legendAllocation}>{segment.allocation.toFixed(1)}%</div>
|
|
388
|
+
</div>
|
|
389
|
+
</div>
|
|
390
|
+
))}
|
|
391
|
+
</div>
|
|
392
|
+
</div>
|
|
393
|
+
</div>
|
|
394
|
+
) : null}
|
|
395
|
+
</div>
|
|
396
|
+
)}
|
|
397
|
+
<div>
|
|
398
|
+
<button
|
|
399
|
+
id="refresh-button"
|
|
400
|
+
style={{
|
|
401
|
+
...styles.viewModeButton,
|
|
402
|
+
...(viewMode === 'chart' ? styles.viewModeButtonActive : {}),
|
|
403
|
+
backgroundColor: viewMode === 'chart' ? branding.colors.primary : branding.colors.surface,
|
|
404
|
+
color: viewMode === 'chart' ? primaryContrast : branding.colors.textSecondary,
|
|
405
|
+
width: '100%',
|
|
406
|
+
marginBlock: '10px',
|
|
407
|
+
}}
|
|
408
|
+
onClick={() => setTriggerEffect(true)}
|
|
409
|
+
>
|
|
410
|
+
Refresh
|
|
411
|
+
</button>
|
|
412
|
+
</div>
|
|
413
|
+
</div>
|
|
414
|
+
);
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
function <%= upperCamel(name) %>Standalone(props: <%= upperCamel(name) %>Props) {
|
|
418
|
+
const sdk = PlatformSDK.getInstance();
|
|
419
|
+
const { data: userContextData } = sdk.useUserContext();
|
|
420
|
+
const { theme: sdkTheme } = sdk.useBranding(DEFAULT_BRANDING_ID);
|
|
421
|
+
const muiTheme = resolveTheme(sdkTheme);
|
|
422
|
+
|
|
423
|
+
return (
|
|
424
|
+
<ThemeProvider theme={muiTheme}>
|
|
425
|
+
<CssBaseline />
|
|
426
|
+
<PortfolioBody {...props} muiTheme={muiTheme} userContextData={userContextData} />
|
|
427
|
+
</ThemeProvider>
|
|
428
|
+
);
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
function <%= upperCamel(name) %>Embedded(props: <%= upperCamel(name) %>Props) {
|
|
432
|
+
const sdk = PlatformSDK.getInstance();
|
|
433
|
+
const { data: userContextData } = sdk.useUserContext();
|
|
434
|
+
sdk.useBranding(DEFAULT_BRANDING_ID);
|
|
435
|
+
const muiTheme = useTheme();
|
|
436
|
+
|
|
437
|
+
return <PortfolioBody {...props} muiTheme={muiTheme} userContextData={userContextData} />;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
const <%= upperCamel(name) %>: React.FC<<%= upperCamel(name) %>Props> = props =>
|
|
441
|
+
props.standalone === true ? (
|
|
442
|
+
<<%= upperCamel(name) %>Standalone {...props} />
|
|
443
|
+
) : (
|
|
444
|
+
<<%= upperCamel(name) %>Embedded {...props} />
|
|
445
|
+
);
|
|
446
|
+
|
|
447
|
+
export default <%= upperCamel(name) %>;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Custom Branding Configuration
|
|
3
|
+
*
|
|
4
|
+
* Defines the branding structure for the investment portfolio widget
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export interface BrandingConfig {
|
|
8
|
+
colors: {
|
|
9
|
+
primary: string;
|
|
10
|
+
secondary: string;
|
|
11
|
+
background: string;
|
|
12
|
+
surface: string;
|
|
13
|
+
text: string;
|
|
14
|
+
textSecondary: string;
|
|
15
|
+
error: string;
|
|
16
|
+
warning: string;
|
|
17
|
+
success: string;
|
|
18
|
+
};
|
|
19
|
+
fonts: {
|
|
20
|
+
primary: string;
|
|
21
|
+
secondary: string;
|
|
22
|
+
};
|
|
23
|
+
spacing: {
|
|
24
|
+
small: number;
|
|
25
|
+
medium: number;
|
|
26
|
+
large: number;
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export const defaultBranding: BrandingConfig = {
|
|
31
|
+
colors: {
|
|
32
|
+
primary: '#1976d2',
|
|
33
|
+
secondary: '#dc004e',
|
|
34
|
+
background: '#ffffff',
|
|
35
|
+
surface: '#f5f5f5',
|
|
36
|
+
text: '#000000',
|
|
37
|
+
textSecondary: '#666666',
|
|
38
|
+
error: '#f44336',
|
|
39
|
+
warning: '#ff9800',
|
|
40
|
+
success: '#4caf50',
|
|
41
|
+
},
|
|
42
|
+
fonts: {
|
|
43
|
+
primary: 'Arial, sans-serif',
|
|
44
|
+
secondary: 'Georgia, serif',
|
|
45
|
+
},
|
|
46
|
+
spacing: {
|
|
47
|
+
small: 8,
|
|
48
|
+
medium: 16,
|
|
49
|
+
large: 24,
|
|
50
|
+
},
|
|
51
|
+
};
|
|
File without changes
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { StrictMode } from 'react';
|
|
2
|
+
import * as ReactDOM from 'react-dom/client';
|
|
3
|
+
|
|
4
|
+
import <%= upperCamel(name) %> from './app/<%= upperCamel(name) %>';
|
|
5
|
+
import { PlatformSDK } from '@cdx-extensions/di-sdk';
|
|
6
|
+
import { WebPlatform } from '@cdx-extensions/di-sdk-web';
|
|
7
|
+
|
|
8
|
+
// When running standalone, this app acts as the host and must register the platform.
|
|
9
|
+
PlatformSDK.init({ platform: WebPlatform.getInstance() });
|
|
10
|
+
|
|
11
|
+
const root = ReactDOM.createRoot(
|
|
12
|
+
document.getElementById('root') as HTMLElement
|
|
13
|
+
);
|
|
14
|
+
root.render(
|
|
15
|
+
<StrictMode>
|
|
16
|
+
<<%= upperCamel(name) %> standalone />
|
|
17
|
+
</StrictMode>
|
|
18
|
+
);
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8" />
|
|
5
|
+
<title><%= upperCamel(name) %></title>
|
|
6
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
7
|
+
<link rel="icon" type="image/x-icon" href="favicon.ico" />
|
|
8
|
+
</head>
|
|
9
|
+
<body>
|
|
10
|
+
<div id="root"></div>
|
|
11
|
+
</body>
|
|
12
|
+
</html>
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"com.ncr.dbk.olb.widgets.investmentportfolio": {
|
|
3
|
+
"widget-title": "investmentportfolio",
|
|
4
|
+
"label": "This is a label."
|
|
5
|
+
},
|
|
6
|
+
"com.ncr.dbk.web.platform": {
|
|
7
|
+
"error": {
|
|
8
|
+
"title": "Something Went Wrong",
|
|
9
|
+
"message": "{{message}}"
|
|
10
|
+
},
|
|
11
|
+
"messages": {
|
|
12
|
+
"generic": {
|
|
13
|
+
"loading": "",
|
|
14
|
+
"error": "We are experiencing some technical difficulties. Please try again later."
|
|
15
|
+
},
|
|
16
|
+
"widget": {
|
|
17
|
+
"loading": "Loading widget...",
|
|
18
|
+
"error": "Error loading widget. Please try again."
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"com.ncr.dbk.olb.widgets.investmentportfolio": {
|
|
3
|
+
"widget-title": "investmentportfolio",
|
|
4
|
+
"label": "Esta es una etiqueta."
|
|
5
|
+
},
|
|
6
|
+
"com.ncr.dbk.web.platform": {
|
|
7
|
+
"error": {
|
|
8
|
+
"title": "Algo salió mal",
|
|
9
|
+
"message": "{{message}}"
|
|
10
|
+
},
|
|
11
|
+
"messages": {
|
|
12
|
+
"generic": {
|
|
13
|
+
"loading": "",
|
|
14
|
+
"error": "Estamos experimentando algunas dificultades técnicas. Por favor, inténtelo de nuevo más tarde."
|
|
15
|
+
},
|
|
16
|
+
"widget": {
|
|
17
|
+
"loading": "Cargando widget...",
|
|
18
|
+
"error": "Error al cargar el widget. Por favor, inténtelo de nuevo."
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"com.ncr.dbk.olb.widgets.investmentportfolio": {
|
|
3
|
+
"widget-title": "investmentportfolio",
|
|
4
|
+
"label": "This is a label."
|
|
5
|
+
},
|
|
6
|
+
"com.ncr.dbk.web.platform": {
|
|
7
|
+
"error": {
|
|
8
|
+
"title": "Quelque chose s’est mal passé",
|
|
9
|
+
"message": "{{message}}"
|
|
10
|
+
},
|
|
11
|
+
"messages": {
|
|
12
|
+
"generic": {
|
|
13
|
+
"loading": "",
|
|
14
|
+
"error": "Nous rencontrons des difficultés techniques. Veuillez réessayer plus tard."
|
|
15
|
+
},
|
|
16
|
+
"widget": {
|
|
17
|
+
"loading": "Loading widget...",
|
|
18
|
+
"error": "Erreur de chargement du widget. Veuillez réessayer."
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import enTranslations from './en-US/translation.json';
|
|
2
|
+
import esTranslations from './es-ES/translation.json';
|
|
3
|
+
import frTranslations from './fr-CA/translation.json';
|
|
4
|
+
import zhTranslations from './zh-TW/translation.json';
|
|
5
|
+
|
|
6
|
+
export default {
|
|
7
|
+
'en-US': enTranslations,
|
|
8
|
+
'es-ES': esTranslations,
|
|
9
|
+
'fr-CA': frTranslations,
|
|
10
|
+
'zh-TW': zhTranslations,
|
|
11
|
+
};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"com.ncr.dbk.olb.widgets.investmentportfolio": {
|
|
3
|
+
"widget-title": "investmentportfolio",
|
|
4
|
+
"label": "這是一個標籤。"
|
|
5
|
+
},
|
|
6
|
+
"com.ncr.dbk.web.platform": {
|
|
7
|
+
"error": {
|
|
8
|
+
"title": "出了點問題",
|
|
9
|
+
"message": "{{message}}"
|
|
10
|
+
},
|
|
11
|
+
"messages": {
|
|
12
|
+
"generic": {
|
|
13
|
+
"loading": "",
|
|
14
|
+
"error": "我們遇到了一些技術困難。請稍後再試。"
|
|
15
|
+
},
|
|
16
|
+
"widget": {
|
|
17
|
+
"loading": "載入小元件...",
|
|
18
|
+
"error": "載入小部件時出錯。請再試一次。"
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import('./bootstrap');
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import '@testing-library/jest-dom';
|
|
2
|
+
import '@testing-library/react';
|
|
3
|
+
|
|
4
|
+
// fixes issue in the CssVarsProvider where window.matchMedia is undefined
|
|
5
|
+
Object.defineProperty(window, 'matchMedia', {
|
|
6
|
+
writable: true,
|
|
7
|
+
value: jest.fn().mockImplementation((query) => ({
|
|
8
|
+
matches: false,
|
|
9
|
+
media: query,
|
|
10
|
+
onchange: null,
|
|
11
|
+
addListener: jest.fn(),
|
|
12
|
+
removeListener: jest.fn(),
|
|
13
|
+
addEventListener: jest.fn(),
|
|
14
|
+
removeEventListener: jest.fn(),
|
|
15
|
+
dispatchEvent: jest.fn(),
|
|
16
|
+
})),
|
|
17
|
+
});
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export interface BffData {
|
|
2
|
+
message: string;
|
|
3
|
+
value: number;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export interface FeatureLinkExGProperty {
|
|
7
|
+
webUrl: string;
|
|
8
|
+
isWebUrlNewWindow?: boolean;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface ImageExGProperty {
|
|
12
|
+
url: string;
|
|
13
|
+
altText: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface <%= upperCamel(name) %>Props {
|
|
17
|
+
/**
|
|
18
|
+
* true = standalone (e.g. local dev); widget uses SDK theme and its own ThemeProvider.
|
|
19
|
+
* false or omitted = embedded in host; widget accepts host theme and does not inject ThemeProvider.
|
|
20
|
+
*/
|
|
21
|
+
standalone?: boolean;
|
|
22
|
+
label?: string;
|
|
23
|
+
date?: number;
|
|
24
|
+
featureLink?: FeatureLinkExGProperty;
|
|
25
|
+
image?: ImageExGProperty;
|
|
26
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": "./tsconfig.json",
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"outDir": "<%= relativeToRoot %>dist/out-tsc",
|
|
5
|
+
"types": [
|
|
6
|
+
"node",
|
|
7
|
+
|
|
8
|
+
"@nx/react/typings/cssmodule.d.ts",
|
|
9
|
+
"@nx/react/typings/image.d.ts"
|
|
10
|
+
]
|
|
11
|
+
},
|
|
12
|
+
"exclude": [
|
|
13
|
+
"jest.config.ts",
|
|
14
|
+
"src/**/*.spec.ts",
|
|
15
|
+
"src/**/*.test.ts",
|
|
16
|
+
"src/**/*.spec.tsx",
|
|
17
|
+
"src/**/*.test.tsx",
|
|
18
|
+
"src/**/*.spec.js",
|
|
19
|
+
"src/**/*.test.js",
|
|
20
|
+
"src/**/*.spec.jsx",
|
|
21
|
+
"src/**/*.test.jsx"
|
|
22
|
+
],
|
|
23
|
+
"include": ["src/**/*.js", "src/**/*.jsx", "src/**/*.ts", "src/**/*.tsx"]
|
|
24
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"jsx": "react-jsx",
|
|
4
|
+
"allowJs": false,
|
|
5
|
+
"esModuleInterop": false,
|
|
6
|
+
"allowSyntheticDefaultImports": true,
|
|
7
|
+
"strict": true,
|
|
8
|
+
"jsxImportSource": "@emotion/react"
|
|
9
|
+
},
|
|
10
|
+
"files": [],
|
|
11
|
+
"include": [],
|
|
12
|
+
"references": [
|
|
13
|
+
{
|
|
14
|
+
"path": "./tsconfig.app.json"
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
"path": "./tsconfig.spec.json"
|
|
18
|
+
}
|
|
19
|
+
],
|
|
20
|
+
"extends": "<%= relativeToRoot %>tsconfig.base.json"
|
|
21
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": "./tsconfig.json",
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"outDir": "<%= relativeToRoot %>dist/out-tsc",
|
|
5
|
+
"module": "commonjs",
|
|
6
|
+
"types": [
|
|
7
|
+
"jest",
|
|
8
|
+
"node",
|
|
9
|
+
"@nx/react/typings/cssmodule.d.ts",
|
|
10
|
+
"@nx/react/typings/image.d.ts"
|
|
11
|
+
]
|
|
12
|
+
},
|
|
13
|
+
"include": [
|
|
14
|
+
"jest.config.ts",
|
|
15
|
+
"src/**/*.test.ts",
|
|
16
|
+
"src/**/*.spec.ts",
|
|
17
|
+
"src/**/*.test.tsx",
|
|
18
|
+
"src/**/*.spec.tsx",
|
|
19
|
+
"src/**/*.test.js",
|
|
20
|
+
"src/**/*.spec.js",
|
|
21
|
+
"src/**/*.test.jsx",
|
|
22
|
+
"src/**/*.spec.jsx",
|
|
23
|
+
"src/**/*.d.ts"
|
|
24
|
+
]
|
|
25
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { composePlugins, withNx } from '@nx/webpack';
|
|
2
|
+
import { withReact } from '@nx/react';
|
|
3
|
+
import { withModuleFederation } from '@nx/react/module-federation';
|
|
4
|
+
|
|
5
|
+
import baseConfig from './module-federation.config';
|
|
6
|
+
|
|
7
|
+
const config = {
|
|
8
|
+
...baseConfig,
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
// Nx plugins for webpack to build config object from Nx options and context.
|
|
12
|
+
/**
|
|
13
|
+
* DTS Plugin is disabled in Nx Workspaces as Nx already provides Typing support Module Federation
|
|
14
|
+
* The DTS Plugin can be enabled by setting dts: true
|
|
15
|
+
* Learn more about the DTS Plugin here: https://module-federation.io/configure/dts.html
|
|
16
|
+
*/
|
|
17
|
+
export default composePlugins(
|
|
18
|
+
withNx(),
|
|
19
|
+
withReact(),
|
|
20
|
+
withModuleFederation(config, { dts: false })
|
|
21
|
+
);
|
package/package.json
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@cdx-di-template/webtemplate-investment-widget",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"license": "MIT",
|
|
5
|
+
"description": "CDX web widget generator template files (investment-widget variant)",
|
|
6
|
+
"publishConfig": {
|
|
7
|
+
"access": "public",
|
|
8
|
+
"registry": "https://registry.npmjs.org/"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"dist"
|
|
12
|
+
],
|
|
13
|
+
"scripts": {
|
|
14
|
+
"build": "node ../../../../tools/scripts/stage-nx-package.js --projectRoot . --outDir dist --copy module-federation.config.ts.template src && node ../../../../tools/scripts/stage-nx-package.js --projectRoot ../../../../tools/widget-di-template-generator/projectFiles --outDir dist --copy . --append"
|
|
15
|
+
}
|
|
16
|
+
}
|