diff --git a/angular.json b/angular.json index 17b2fb86e..c21159c7e 100644 --- a/angular.json +++ b/angular.json @@ -245,9 +245,14 @@ }, "ui": { "projectType": "library", + "schematics": { + "@schematics/angular:component": { + "style": "scss" + } + }, "root": "libs/ui", "sourceRoot": "libs/ui/src", - "prefix": "ghostfolio", + "prefix": "gf", "architect": { "test": { "builder": "@nrwl/jest:jest", diff --git a/apps/ui-e2e/src/integration/value/value.component.spec.ts b/apps/ui-e2e/src/integration/value/value.component.spec.ts new file mode 100644 index 000000000..5b90784e7 --- /dev/null +++ b/apps/ui-e2e/src/integration/value/value.component.spec.ts @@ -0,0 +1,6 @@ +describe('ui', () => { + beforeEach(() => cy.visit('/iframe.html?id=valuecomponent--loading')); + it('should render the component', () => { + cy.get('gf-value').should('exist'); + }); +}); diff --git a/libs/ui/src/lib/ui.module.ts b/libs/ui/src/lib/ui.module.ts index 0f26260a1..c41054ccc 100644 --- a/libs/ui/src/lib/ui.module.ts +++ b/libs/ui/src/lib/ui.module.ts @@ -1,7 +1,15 @@ import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; +import { ValueComponent } from './value/value.component'; +// import { GfValueModule } from './value/value.module'; @NgModule({ - imports: [CommonModule] + imports: [CommonModule/*, GfValueModule*/], + declarations: [ + ValueComponent + ], + exports: [ + ValueComponent + ] }) export class UiModule {} diff --git a/libs/ui/src/lib/value/value.component.html b/libs/ui/src/lib/value/value.component.html new file mode 100644 index 000000000..cbaa85379 --- /dev/null +++ b/libs/ui/src/lib/value/value.component.html @@ -0,0 +1,46 @@ + +
+ +
+
+
-
+
+ {{ formattedValue }}% +
+
+ + *** + + + {{ formattedValue }} + +
+ + {{ currency }} + +
+ {{ currency }} +
+
+ +
+ {{ formattedDate }} +
+
+
+ + {{ label }} + +
+ + + \ No newline at end of file diff --git a/libs/ui/src/lib/value/value.component.scss b/libs/ui/src/lib/value/value.component.scss new file mode 100644 index 000000000..7452006e0 --- /dev/null +++ b/libs/ui/src/lib/value/value.component.scss @@ -0,0 +1,5 @@ +:host { + display: flex; + flex-direction: column; + font-variant-numeric: tabular-nums; +} diff --git a/libs/ui/src/lib/value/value.component.spec.ts b/libs/ui/src/lib/value/value.component.spec.ts new file mode 100644 index 000000000..8ae136138 --- /dev/null +++ b/libs/ui/src/lib/value/value.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { ValueComponent } from './value.component'; + +describe('ValueComponent', () => { + let component: ValueComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ ValueComponent ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(ValueComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/libs/ui/src/lib/value/value.component.stories.ts b/libs/ui/src/lib/value/value.component.stories.ts new file mode 100644 index 000000000..7bd7fe156 --- /dev/null +++ b/libs/ui/src/lib/value/value.component.stories.ts @@ -0,0 +1,38 @@ +import { moduleMetadata, Story, Meta } from '@storybook/angular'; +import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader'; +import { ValueComponent } from './value.component'; + +export default { + title: 'Value', + component: ValueComponent, + decorators: [ + moduleMetadata({ + imports: [NgxSkeletonLoaderModule], + }) + ], +} as Meta; + +const Template: Story = (args: ValueComponent) => ({ + props: args, +}); + +export const Loading = Template.bind({}); +Loading.args = { + value: undefined +} + +export const Integer = Template.bind({}); +Integer.args = { + isInteger: true, + locale: 'en', + value: 7 +} + +export const Currency = Template.bind({}); +Currency.args = { + currency: 'USD', + isInteger: true, + label: 'Label', + locale: 'en', + value: 7 +} diff --git a/libs/ui/src/lib/value/value.component.ts b/libs/ui/src/lib/value/value.component.ts new file mode 100644 index 000000000..1d7e564e7 --- /dev/null +++ b/libs/ui/src/lib/value/value.component.ts @@ -0,0 +1,108 @@ +import { + ChangeDetectionStrategy, + Component, + Input, + OnChanges, +} from '@angular/core'; +import { DEFAULT_DATE_FORMAT } from '@ghostfolio/common/config'; +import { format, isDate } from 'date-fns'; +import { isNumber } from 'lodash'; + +@Component({ + selector: 'gf-value', + changeDetection: ChangeDetectionStrategy.OnPush, + templateUrl: './value.component.html', + styleUrls: ['./value.component.scss'] +}) +export class ValueComponent implements OnChanges { + @Input() colorizeSign = false; + @Input() currency = ''; + @Input() isCurrency = false; + @Input() isInteger = false; + @Input() isPercent = false; + @Input() label = ''; + @Input() locale = ''; + @Input() position = ''; + @Input() size = ''; + @Input() value: number | string = ''; + + public absoluteValue = 0; + public formattedDate = ''; + public formattedValue = ''; + public isDate = false; + public isNumber = false; + public useAbsoluteValue = false; + + public constructor() {} + + public ngOnChanges() { + if (this.value || this.value === 0) { + if (isNumber(this.value)) { + this.isDate = false; + this.isNumber = true; + this.absoluteValue = Math.abs(this.value); + + if (this.colorizeSign) { + this.useAbsoluteValue = true; + if (this.currency || this.isCurrency) { + try { + this.formattedValue = this.absoluteValue.toLocaleString( + this.locale, + { + maximumFractionDigits: 2, + minimumFractionDigits: 2 + } + ); + } catch {} + } else if (this.isPercent) { + try { + this.formattedValue = (this.absoluteValue * 100).toLocaleString( + this.locale, + { + maximumFractionDigits: 2, + minimumFractionDigits: 2 + } + ); + } catch {} + } + } else if (this.isPercent) { + try { + this.formattedValue = (this.value * 100).toLocaleString( + this.locale, + { + maximumFractionDigits: 2, + minimumFractionDigits: 2 + } + ); + } catch {} + } else if (this.currency || this.isCurrency) { + try { + this.formattedValue = this.value?.toLocaleString(this.locale, { + maximumFractionDigits: 2, + minimumFractionDigits: 2 + }); + } catch {} + } else if (this.isInteger) { + try { + this.formattedValue = this.value?.toLocaleString(this.locale, { + maximumFractionDigits: 0, + minimumFractionDigits: 0 + }); + } catch {} + } + } else { + try { + if (isDate(new Date(this.value))) { + this.isDate = true; + this.isNumber = false; + + this.formattedDate = format( + new Date(this.value), + DEFAULT_DATE_FORMAT + ); + } + } catch {} + } + } + } +} diff --git a/libs/ui/src/lib/value/value.module.ts b/libs/ui/src/lib/value/value.module.ts new file mode 100644 index 000000000..9d3c9aedb --- /dev/null +++ b/libs/ui/src/lib/value/value.module.ts @@ -0,0 +1,13 @@ +import { CommonModule } from '@angular/common'; +import { NgModule } from '@angular/core'; +import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader'; + +import { ValueComponent } from './value.component'; + +@NgModule({ + declarations: [ValueComponent], + exports: [ValueComponent], + imports: [CommonModule, NgxSkeletonLoaderModule], + providers: [] +}) +export class GfValueModule {} diff --git a/package.json b/package.json index 05f99c42a..4d34bc5a8 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,7 @@ "affected:test": "nx affected:test", "angular": "node --max_old_space_size=32768 ./node_modules/@angular/cli/bin/ng", "build:all": "ng build --configuration production api && ng build --configuration production client && yarn replace-placeholders-in-build", + "build:storybook": "nx run ui:build-storybook", "clean": "rimraf dist", "database:format-schema": "prisma format", "database:generate-typings": "prisma generate", @@ -37,6 +38,7 @@ "start:client": "ng serve client --hmr -o", "start:prod": "node apps/api/main", "start:server": "nx serve api --watch", + "start:storybook": "nx run ui:storybook", "test": "nx test", "ts-node": "ts-node --compiler-options '{\"module\":\"CommonJS\"}'", "update": "nx migrate latest",