From 6a6463ffd27ffbeef6675a4a56932b1cc7a6bf44 Mon Sep 17 00:00:00 2001 From: Andrii Kostenko Date: Fri, 27 Jun 2025 18:23:56 +0000 Subject: [PATCH 01/10] phone number widget --- backend/src/enums/widget-type.enum.ts | 1 + frontend/CLAUDE.md | 409 ++++++++++++++++++ frontend/package.json | 4 + .../db-table-widgets.component.ts | 11 + .../row-fields/phone/phone.component.css | 23 + .../row-fields/phone/phone.component.html | 21 + .../row-fields/phone/phone.component.ts | 75 ++++ frontend/src/app/consts/field-types.ts | 2 + frontend/yarn.lock | 49 +++ .../shared/enums/table-widget-type.enum.ts | 3 +- 10 files changed, 597 insertions(+), 1 deletion(-) create mode 100644 frontend/CLAUDE.md create mode 100644 frontend/src/app/components/ui-components/row-fields/phone/phone.component.css create mode 100644 frontend/src/app/components/ui-components/row-fields/phone/phone.component.html create mode 100644 frontend/src/app/components/ui-components/row-fields/phone/phone.component.ts diff --git a/backend/src/enums/widget-type.enum.ts b/backend/src/enums/widget-type.enum.ts index 22740eea4..36e569fff 100644 --- a/backend/src/enums/widget-type.enum.ts +++ b/backend/src/enums/widget-type.enum.ts @@ -17,4 +17,5 @@ export enum WidgetTypeEnum { Image = 'Image', URL = 'URL', Code = 'Code', + Phone = 'Phone', } diff --git a/frontend/CLAUDE.md b/frontend/CLAUDE.md new file mode 100644 index 000000000..53e336e04 --- /dev/null +++ b/frontend/CLAUDE.md @@ -0,0 +1,409 @@ +# Auto-Admin Frontend Development Guide + +## ๐Ÿš€ Project Overview + +This is the frontend for Auto-Admin, a database administration tool built with Angular 19. The application provides a comprehensive interface for managing database connections, tables, and data with customizable widgets and user permissions. + +## ๐Ÿ›  Technologies & Frameworks + +### Core Technologies +- **Angular**: v19.0.4 (Latest with standalone components) +- **TypeScript**: v5.6.0 (ES2022 target) +- **RxJS**: v7.4.0 (Reactive programming) +- **Node.js**: Modern versions supported + +### UI/UX Stack +- **Angular Material**: v19.0.3 (Primary UI library) +- **Angular CDK**: v19.0.3 (Component Development Kit) +- **@brumeilde/ngx-theme**: v1.2.1 (Custom theming) +- **SCSS**: Material Design theming +- **ngx-bootstrap**: v19.0.2 (Additional components) + +### Additional Libraries +- **@ngstack/code-editor**: v9.0.0 (Code editing) +- **ngx-markdown**: v19.0.0 (Markdown rendering) +- **ngx-stripe**: v19.0.0 (Payment integration) +- **angulartics2**: v14.1.0 (Analytics) +- **@sentry/angular-ivy**: v7.116.0 (Error monitoring) + +## ๐Ÿ“ Project Structure + +``` +src/app/ +โ”œโ”€โ”€ components/ +โ”‚ โ”œโ”€โ”€ dashboard/ # Main data interface +โ”‚ โ”œโ”€โ”€ ui-components/ # Reusable UI components +โ”‚ โ”‚ โ”œโ”€โ”€ row-fields/ # Widget components (text, date, select, etc.) +โ”‚ โ”‚ โ”œโ”€โ”€ filter-fields/ # Filter components +โ”‚ โ”‚ โ””โ”€โ”€ alert/ # Common UI elements +โ”‚ โ”œโ”€โ”€ login/ # Authentication +โ”‚ โ”œโ”€โ”€ registration/ # User registration +โ”‚ โ”œโ”€โ”€ connect-db/ # Database connection setup +โ”‚ โ”œโ”€โ”€ company/ # Company management +โ”‚ โ””โ”€โ”€ skeletons/ # Loading placeholders +โ”œโ”€โ”€ services/ # Business logic & API calls +โ”œโ”€โ”€ models/ # TypeScript interfaces +โ”œโ”€โ”€ consts/ # Constants & configurations +โ”œโ”€โ”€ lib/ # Utility functions +โ”œโ”€โ”€ validators/ # Form validation +โ””โ”€โ”€ directives/ # Custom directives +``` + +## ๐Ÿ— Architecture Patterns + +### Standalone Components +Uses Angular 19's standalone component architecture: + +```typescript +@Component({ + selector: 'app-example', + templateUrl: './example.component.html', + styleUrls: ['./example.component.css'], + imports: [CommonModule, MatButtonModule, FormsModule], // Direct imports +}) +export class ExampleComponent implements OnInit { + // Component logic +} +``` + +### Service-Based State Management +No NgRx - uses BehaviorSubject pattern for state: + +```typescript +@Injectable({ providedIn: 'root' }) +export class DataService { + private dataSubject = new BehaviorSubject(''); + public cast = this.dataSubject.asObservable(); + + updateData(newData: any) { + this.dataSubject.next(newData); + } +} +``` + +### Dynamic Widget System +Highly flexible widget system for different data types: + +```typescript +export const UIwidgets = { + String: TextRowComponent, + Number: NumberRowComponent, + Date: DateRowComponent, + Boolean: BooleanRowComponent, + Phone: PhoneRowComponent, + // ... more widgets +}; +``` + +## ๐Ÿ“ Code Style & Conventions + +### Naming Conventions +- **Files**: `kebab-case.component.ts` +- **Classes**: `PascalCase` (e.g., `DbTableSettingsComponent`) +- **Methods**: `camelCase` with verb-noun pattern +- **Variables**: `camelCase` descriptive names +- **Constants**: `UPPER_SNAKE_CASE` +- **Private members**: Prefixed with `_` (e.g., `_privateMethod`) +- **Selectors**: `app-` prefix with kebab-case + +### Component Structure +```typescript +@Component({ + selector: 'app-widget-name', + templateUrl: './widget-name.component.html', + styleUrls: ['./widget-name.component.css'], + imports: [CommonModule, MatModule, ...], // All required imports +}) +export class WidgetNameComponent implements OnInit { + // Input/Output properties first + @Input() inputProperty: string; + @Output() outputEvent = new EventEmitter(); + + // Public properties + public publicProperty: string; + + // Private properties + private _privateProperty: string; + + // Lifecycle hooks + ngOnInit(): void { + // Initialization logic + } + + // Public methods + public handleClick(): void { + // Event handling + } + + // Private methods + private _helperMethod(): void { + // Internal logic + } +} +``` + +### Import Organization +```typescript +// External libraries first +import { Component, OnInit, Input, Output, EventEmitter } from '@angular/core'; +import { MatButtonModule } from '@angular/material/button'; +import { CommonModule } from '@angular/common'; + +// Internal imports second +import { BaseRowFieldComponent } from '../base-row-field/base-row-field.component'; +import { DataService } from 'src/app/services/data.service'; +``` + +## ๐ŸŽจ Styling Guidelines + +### Material Design Implementation +- Uses Material Design 2 (M2) APIs +- Custom theme with light/dark mode support +- Noto Sans font family + +### SCSS Structure +```scss +// Component-specific styles +.component-name { + &__element { + // BEM methodology + } + + &--modifier { + // State variations + } +} + +// Use Material Design spacing +.mat-form-field { + width: 100%; + margin-bottom: 16px; +} +``` + +### Theme Configuration +```scss +$custom-palette-primary: mat.m2-define-palette(mat.$m2-blue-palette); +$custom-light-theme: mat.m2-define-light-theme(( + color: ( + primary: $custom-palette-primary, + accent: $custom-palette-accent, + warn: $custom-palette-warn, + ), + density: -3, +)); +``` + +## ๐Ÿงช Testing Guidelines + +### Test Structure +```typescript +describe('ComponentName', () => { + let component: ComponentName; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [ComponentName, MaterialModules, ...], + providers: [provideHttpClient(), MockServices, ...] + }).compileComponents(); + + fixture = TestBed.createComponent(ComponentName); + component = fixture.componentInstance; + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should handle user input', () => { + // Test user interactions + }); +}); +``` + +### Test Commands +```bash +npm test # Run tests in watch mode +npm run test:ci # Run tests once for CI +``` + +## ๐Ÿ”ง Development Workflow + +### Development Commands +```bash +npm start # Start development server +npm run build # Build for production +npm test # Run tests +npm run lint # Run linting (TSLint - needs ESLint migration) +``` + +### Environment Configuration +- `environment.ts` - Development +- `environment.prod.ts` - Production +- `environment.saas.ts` - SaaS development +- `environment.saas-prod.ts` - SaaS production + +## ๐ŸŽฏ Widget Development Guide + +### Creating New Widgets + +1. **Create Component Files**: +```bash +mkdir src/app/components/ui-components/row-fields/your-widget +``` + +2. **Extend Base Component**: +```typescript +export class YourWidgetComponent extends BaseRowFieldComponent { + @Input() value: any; + static type = 'your-widget'; + + // Widget-specific logic +} +``` + +3. **Add to Widget Registry**: +```typescript +// In src/app/consts/field-types.ts +export const UIwidgets = { + // ... existing widgets + YourWidget: YourWidgetComponent, +}; +``` + +4. **Add to Enums**: +```typescript +// Backend: src/enums/widget-type.enum.ts +// Shared: shared-code/src/data-access-layer/shared/enums/table-widget-type.enum.ts +export enum WidgetTypeEnum { + // ... existing types + YourWidget = 'YourWidget', +} +``` + +5. **Configure Default Parameters**: +```typescript +// In db-table-widgets.component.ts +public defaultParams = { + // ... existing params + YourWidget: `// Widget configuration documentation +{ + "param1": "value1", + "param2": true +}`, +}; +``` + +## ๐Ÿšจ Common Patterns & Best Practices + +### Error Handling +```typescript +this.service.getData().subscribe({ + next: (data) => { + // Handle success + }, + error: (error) => { + console.error('Error occurred:', error); + // Show user-friendly error message + } +}); +``` + +### Loading States +```typescript +export class ExampleComponent { + public loading = false; + + async loadData() { + this.loading = true; + try { + const data = await this.service.getData().toPromise(); + // Process data + } finally { + this.loading = false; + } + } +} +``` + +### Form Validation +```typescript +// Use Angular's reactive forms with Material Design +import { FormBuilder, Validators } from '@angular/forms'; + +constructor(private fb: FormBuilder) { + this.form = this.fb.group({ + field: ['', [Validators.required, Validators.minLength(3)]] + }); +} +``` + +## ๐Ÿ“ฆ Package Management + +### Using Yarn +```bash +yarn add package-name # Add dependency +yarn add -D package-name # Add dev dependency +yarn install # Install dependencies +``` + +### Important: Peer Dependencies +When adding new packages, check for peer dependency warnings and install missing dependencies: +```bash +yarn add ngx-intl-tel-input intl-tel-input google-libphonenumber +``` + +## ๐Ÿ”„ Migration Notes + +### Recommended Upgrades +- **TSLint โ†’ ESLint**: Current linting setup uses deprecated TSLint +- **Material Design 3**: Consider upgrading from M2 to M3 APIs +- **Standalone Migration**: Already using standalone components (โœ…) + +### Breaking Changes to Watch +- Angular Material API changes between versions +- RxJS operator deprecations +- TypeScript strict mode compliance + +## ๐ŸŽฏ Performance Considerations + +### Bundle Optimization +- Tree-shaking enabled in production builds +- Lazy loading for route-based features +- OnPush change detection where applicable + +### Memory Management +```typescript +export class ExampleComponent implements OnDestroy { + private destroy$ = new Subject(); + + ngOnInit() { + this.service.data$.pipe( + takeUntil(this.destroy$) + ).subscribe(data => { + // Handle data + }); + } + + ngOnDestroy() { + this.destroy$.next(); + this.destroy$.complete(); + } +} +``` + +--- + +## ๐Ÿค Contributing + +When contributing to this project: + +1. Follow the established naming conventions +2. Extend `BaseRowFieldComponent` for new widgets +3. Add comprehensive tests for new features +4. Update this CLAUDE.md file when adding new patterns +5. Ensure builds pass with `npm run build` +6. Follow the widget development guide for UI components + +For questions about implementation patterns, refer to existing similar components in the codebase as examples. \ No newline at end of file diff --git a/frontend/package.json b/frontend/package.json index 601e74f8d..6a7bd02fb 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -39,11 +39,15 @@ "angular-password-strength-meter": "^12.0.0", "angulartics2": "^14.1.0", "date-fns": "^4.1.0", + "google-libphonenumber": "^3.2.42", + "intl-tel-input": "^25.3.1", "ipaddr.js": "^2.2.0", "json5": "^2.2.3", "lodash": "^4.17.21", "ng-dynamic-component": "^10.7.0", + "ngx-bootstrap": "^19.0.2", "ngx-cookie-service": "^19.0.0", + "ngx-intl-tel-input": "^17.0.0", "ngx-markdown": "^19.0.0", "ngx-stripe": "^19.0.0", "pluralize": "^8.0.0", diff --git a/frontend/src/app/components/dashboard/db-table-widgets/db-table-widgets.component.ts b/frontend/src/app/components/dashboard/db-table-widgets/db-table-widgets.component.ts index 0fc18cc95..f20de3d84 100644 --- a/frontend/src/app/components/dashboard/db-table-widgets/db-table-widgets.component.ts +++ b/frontend/src/app/components/dashboard/db-table-widgets/db-table-widgets.component.ts @@ -162,6 +162,17 @@ export class DbTableWidgetsComponent implements OnInit { } `, URL: `// No settings required`, + Phone: +`// Configure international phone number widget +// example: +{ + "preferred_countries": ["US", "GB", "CA"], + "enable_placeholder": true, + "enable_auto_country_select": true, + "phone_validation": true, + "format": "international" +} +`, Foreign_key: `// Provide settings for foreign key widget { "column_name": "", // copy the name of the column you selected diff --git a/frontend/src/app/components/ui-components/row-fields/phone/phone.component.css b/frontend/src/app/components/ui-components/row-fields/phone/phone.component.css new file mode 100644 index 000000000..593dcec59 --- /dev/null +++ b/frontend/src/app/components/ui-components/row-fields/phone/phone.component.css @@ -0,0 +1,23 @@ +.phone-form-field { + width: 100%; +} + +.phone-form-field ::ng-deep .ngx-int-tel-input { + width: 100%; +} + +.phone-form-field ::ng-deep .ngx-int-tel-input .form-control { + border: none; + outline: none; + box-shadow: none; + padding: 0; + font-size: inherit; + font-family: inherit; + color: inherit; + background: transparent; +} + +.phone-form-field ::ng-deep .ngx-int-tel-input .country-dropdown { + border: none; + background: transparent; +} \ No newline at end of file diff --git a/frontend/src/app/components/ui-components/row-fields/phone/phone.component.html b/frontend/src/app/components/ui-components/row-fields/phone/phone.component.html new file mode 100644 index 000000000..9e3b211ce --- /dev/null +++ b/frontend/src/app/components/ui-components/row-fields/phone/phone.component.html @@ -0,0 +1,21 @@ + + {{normalizedLabel}} + + + \ No newline at end of file diff --git a/frontend/src/app/components/ui-components/row-fields/phone/phone.component.ts b/frontend/src/app/components/ui-components/row-fields/phone/phone.component.ts new file mode 100644 index 000000000..ba98bb6c5 --- /dev/null +++ b/frontend/src/app/components/ui-components/row-fields/phone/phone.component.ts @@ -0,0 +1,75 @@ +import { Component, Injectable, Input, OnInit, AfterViewInit } from '@angular/core'; +import { NgxIntlTelInputModule } from 'ngx-intl-tel-input'; +import { CountryISO, SearchCountryField, PhoneNumberFormat } from 'ngx-intl-tel-input'; + +import { BaseRowFieldComponent } from '../base-row-field/base-row-field.component'; +import { FormsModule } from '@angular/forms'; +import { MatFormFieldModule } from '@angular/material/form-field'; + +@Injectable() + +@Component({ + selector: 'app-row-phone', + templateUrl: './phone.component.html', + styleUrls: ['./phone.component.css'], + imports: [MatFormFieldModule, FormsModule, NgxIntlTelInputModule] +}) +export class PhoneRowComponent extends BaseRowFieldComponent implements OnInit, AfterViewInit { + @Input() value: string; + + static type = 'phone'; + + SearchCountryField = SearchCountryField; + CountryISO = CountryISO; + PhoneNumberFormat = PhoneNumberFormat; + preferredCountries: CountryISO[] = [CountryISO.UnitedStates, CountryISO.UnitedKingdom]; + enablePlaceholder: boolean = true; + enableAutoCountrySelect: boolean = true; + phoneValidation: boolean = true; + numberFormat: PhoneNumberFormat = PhoneNumberFormat.International; + + ngOnInit(): void { + super.ngOnInit(); + this.configureFromWidgetParams(); + } + + ngAfterViewInit(): void { + // Any additional initialization if needed + } + + private configureFromWidgetParams(): void { + if (this.widgetStructure && this.widgetStructure.widget_params) { + const params = this.widgetStructure.widget_params; + + if (params.preferred_countries && Array.isArray(params.preferred_countries)) { + this.preferredCountries = params.preferred_countries.map(country => CountryISO[country as keyof typeof CountryISO] || CountryISO.UnitedStates); + } + + if (typeof params.enable_placeholder === 'boolean') { + this.enablePlaceholder = params.enable_placeholder; + } + + if (typeof params.enable_auto_country_select === 'boolean') { + this.enableAutoCountrySelect = params.enable_auto_country_select; + } + + if (typeof params.phone_validation === 'boolean') { + this.phoneValidation = params.phone_validation; + } + + if (params.format === 'national') { + this.numberFormat = PhoneNumberFormat.National; + } + } + } + + onPhoneNumberChange(phoneNumber: any): void { + if (phoneNumber && phoneNumber.internationalNumber) { + this.value = phoneNumber.internationalNumber; + this.onFieldChange.emit(this.value); + } else { + this.value = ''; + this.onFieldChange.emit(this.value); + } + } +} \ No newline at end of file diff --git a/frontend/src/app/consts/field-types.ts b/frontend/src/app/consts/field-types.ts index 4262b1179..9d8b335b0 100644 --- a/frontend/src/app/consts/field-types.ts +++ b/frontend/src/app/consts/field-types.ts @@ -18,6 +18,7 @@ import { FileRowComponent } from '../components/ui-components/row-fields/file/fi import { CodeRowComponent } from '../components/ui-components/row-fields/code/code.component'; import { ImageRowComponent } from '../components/ui-components/row-fields/image/image.component'; import { UrlRowComponent } from '../components/ui-components/row-fields/url/url.component'; +import { PhoneRowComponent } from '../components/ui-components/row-fields/phone/phone.component'; export const timestampTypes = ['timestamp without time zone', 'timestamp with time zone', 'timestamp', 'date', 'time without time zone', 'time with time zone' , 'time', 'datetime', 'date time', 'datetime2', 'datetimeoffset', 'curdate', 'curtime', 'now', 'localtime', 'localtimestamp']; export const defaultTimestampValues = { @@ -42,6 +43,7 @@ export const UIwidgets = { Code: CodeRowComponent, Image: ImageRowComponent, URL: UrlRowComponent, + Phone: PhoneRowComponent, Foreign_key: ForeignKeyRowComponent, } diff --git a/frontend/yarn.lock b/frontend/yarn.lock index aa57a9d1e..6cc2acbc9 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -6243,6 +6243,8 @@ __metadata: angular-password-strength-meter: ^12.0.0 angulartics2: ^14.1.0 date-fns: ^4.1.0 + google-libphonenumber: ^3.2.42 + intl-tel-input: ^25.3.1 ipaddr.js: ^2.2.0 jasmine-core: ~5.5.0 jasmine-spec-reporter: ~7.0.0 @@ -6255,7 +6257,9 @@ __metadata: karma-jasmine-html-reporter: ^2.1.0 lodash: ^4.17.21 ng-dynamic-component: ^10.7.0 + ngx-bootstrap: ^19.0.2 ngx-cookie-service: ^19.0.0 + ngx-intl-tel-input: ^17.0.0 ngx-markdown: ^19.0.0 ngx-stripe: ^19.0.0 pluralize: ^8.0.0 @@ -7354,6 +7358,13 @@ __metadata: languageName: node linkType: hard +"google-libphonenumber@npm:^3.2.42": + version: 3.2.42 + resolution: "google-libphonenumber@npm:3.2.42" + checksum: f98ea03d7509ccef535a6490089895ae2ae03b49c8c3a3c604d94184064379442357c58675759f12f70be8c6ea8c224799ea751830971fb54a116c3751241657 + languageName: node + linkType: hard + "gopd@npm:^1.2.0": version: 1.2.0 resolution: "gopd@npm:1.2.0" @@ -7722,6 +7733,13 @@ __metadata: languageName: node linkType: hard +"intl-tel-input@npm:^25.3.1": + version: 25.3.1 + resolution: "intl-tel-input@npm:25.3.1" + checksum: f4bb9cd2cfe8370ab6f5c4836747bd22a301ed9cc2c4987b0760135e1b0101ab87c5093796b789e09fab3c6e787bda12dbfb015bdade239809447dcaeab91aae + languageName: node + linkType: hard + "ip-address@npm:^9.0.5": version: 9.0.5 resolution: "ip-address@npm:9.0.5" @@ -9211,6 +9229,21 @@ __metadata: languageName: node linkType: hard +"ngx-bootstrap@npm:^19.0.2": + version: 19.0.2 + resolution: "ngx-bootstrap@npm:19.0.2" + dependencies: + tslib: ^2.3.0 + peerDependencies: + "@angular/animations": ^19.0.1 + "@angular/common": ^19.0.1 + "@angular/core": ^19.0.1 + "@angular/forms": ^19.0.1 + rxjs: ^6.5.3 || ^7.4.0 + checksum: d919e82d827fb2171e36e8a62a631ebe2113c162af325f7e43f38295e3ce84c83b6d8d34a7131393150b2343619309ae33bece0ca7ae75a6631797bf2a8a3429 + languageName: node + linkType: hard + "ngx-cookie-service@npm:^19.0.0": version: 19.0.0 resolution: "ngx-cookie-service@npm:19.0.0" @@ -9223,6 +9256,22 @@ __metadata: languageName: node linkType: hard +"ngx-intl-tel-input@npm:^17.0.0": + version: 17.0.0 + resolution: "ngx-intl-tel-input@npm:17.0.0" + dependencies: + tslib: ^2.3.0 + peerDependencies: + "@angular/common": ">= 17.0.0" + "@angular/core": ">= 17.0.0" + "@angular/forms": ">= 17.0.0" + google-libphonenumber: ^3.0.0 + intl-tel-input: ^19.0.0 + ngx-bootstrap: ">= 12.0.0" + checksum: 8d1a7d8545118c8e79e0f073c75a73a5fead30c64df45831f0e27cc78d6d8c8042490893f273ff65611e5b4d5d07b5b85f7c8a823b1463474080dd0a6b27084a + languageName: node + linkType: hard + "ngx-markdown@npm:^19.0.0": version: 19.0.0 resolution: "ngx-markdown@npm:19.0.0" diff --git a/shared-code/src/data-access-layer/shared/enums/table-widget-type.enum.ts b/shared-code/src/data-access-layer/shared/enums/table-widget-type.enum.ts index 94c63cc9a..cb70b6680 100644 --- a/shared-code/src/data-access-layer/shared/enums/table-widget-type.enum.ts +++ b/shared-code/src/data-access-layer/shared/enums/table-widget-type.enum.ts @@ -13,5 +13,6 @@ export enum TableWidgetTypeEnum { UUID = 'UUID', Enum = 'Enum', Foreign_key = 'Foreign_key', - File = 'File' + File = 'File', + Phone = 'Phone' } \ No newline at end of file From d62dcf266c1bae5ecfd0042039ca3fe00046d92a Mon Sep 17 00:00:00 2001 From: Andrii Kostenko Date: Sat, 28 Jun 2025 20:39:18 +0000 Subject: [PATCH 02/10] material phone number input --- frontend/package.json | 5 +--- .../row-fields/phone/phone.component.html | 17 ++++------- .../row-fields/phone/phone.component.ts | 28 +++++-------------- 3 files changed, 13 insertions(+), 37 deletions(-) diff --git a/frontend/package.json b/frontend/package.json index 6a7bd02fb..49e239601 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -39,15 +39,12 @@ "angular-password-strength-meter": "^12.0.0", "angulartics2": "^14.1.0", "date-fns": "^4.1.0", - "google-libphonenumber": "^3.2.42", - "intl-tel-input": "^25.3.1", "ipaddr.js": "^2.2.0", "json5": "^2.2.3", "lodash": "^4.17.21", "ng-dynamic-component": "^10.7.0", - "ngx-bootstrap": "^19.0.2", "ngx-cookie-service": "^19.0.0", - "ngx-intl-tel-input": "^17.0.0", + "ngx-mat-input-tel": "^9.0.0", "ngx-markdown": "^19.0.0", "ngx-stripe": "^19.0.0", "pluralize": "^8.0.0", diff --git a/frontend/src/app/components/ui-components/row-fields/phone/phone.component.html b/frontend/src/app/components/ui-components/row-fields/phone/phone.component.html index 9e3b211ce..2a735e421 100644 --- a/frontend/src/app/components/ui-components/row-fields/phone/phone.component.html +++ b/frontend/src/app/components/ui-components/row-fields/phone/phone.component.html @@ -1,21 +1,14 @@ {{normalizedLabel}} - - + (valueChange)="onPhoneNumberChange($event)"> + \ No newline at end of file diff --git a/frontend/src/app/components/ui-components/row-fields/phone/phone.component.ts b/frontend/src/app/components/ui-components/row-fields/phone/phone.component.ts index ba98bb6c5..352038906 100644 --- a/frontend/src/app/components/ui-components/row-fields/phone/phone.component.ts +++ b/frontend/src/app/components/ui-components/row-fields/phone/phone.component.ts @@ -1,6 +1,5 @@ import { Component, Injectable, Input, OnInit, AfterViewInit } from '@angular/core'; -import { NgxIntlTelInputModule } from 'ngx-intl-tel-input'; -import { CountryISO, SearchCountryField, PhoneNumberFormat } from 'ngx-intl-tel-input'; +import { NgxMatInputTelComponent } from 'ngx-mat-input-tel'; import { BaseRowFieldComponent } from '../base-row-field/base-row-field.component'; import { FormsModule } from '@angular/forms'; @@ -12,21 +11,17 @@ import { MatFormFieldModule } from '@angular/material/form-field'; selector: 'app-row-phone', templateUrl: './phone.component.html', styleUrls: ['./phone.component.css'], - imports: [MatFormFieldModule, FormsModule, NgxIntlTelInputModule] + imports: [MatFormFieldModule, FormsModule, NgxMatInputTelComponent] }) export class PhoneRowComponent extends BaseRowFieldComponent implements OnInit, AfterViewInit { @Input() value: string; static type = 'phone'; - SearchCountryField = SearchCountryField; - CountryISO = CountryISO; - PhoneNumberFormat = PhoneNumberFormat; - preferredCountries: CountryISO[] = [CountryISO.UnitedStates, CountryISO.UnitedKingdom]; + preferredCountries: string[] = ['US', 'GB']; enablePlaceholder: boolean = true; enableAutoCountrySelect: boolean = true; phoneValidation: boolean = true; - numberFormat: PhoneNumberFormat = PhoneNumberFormat.International; ngOnInit(): void { super.ngOnInit(); @@ -42,7 +37,7 @@ export class PhoneRowComponent extends BaseRowFieldComponent implements OnInit, const params = this.widgetStructure.widget_params; if (params.preferred_countries && Array.isArray(params.preferred_countries)) { - this.preferredCountries = params.preferred_countries.map(country => CountryISO[country as keyof typeof CountryISO] || CountryISO.UnitedStates); + this.preferredCountries = params.preferred_countries; } if (typeof params.enable_placeholder === 'boolean') { @@ -56,20 +51,11 @@ export class PhoneRowComponent extends BaseRowFieldComponent implements OnInit, if (typeof params.phone_validation === 'boolean') { this.phoneValidation = params.phone_validation; } - - if (params.format === 'national') { - this.numberFormat = PhoneNumberFormat.National; - } } } - onPhoneNumberChange(phoneNumber: any): void { - if (phoneNumber && phoneNumber.internationalNumber) { - this.value = phoneNumber.internationalNumber; - this.onFieldChange.emit(this.value); - } else { - this.value = ''; - this.onFieldChange.emit(this.value); - } + onPhoneNumberChange(phoneNumber: string): void { + this.value = phoneNumber; + this.onFieldChange.emit(this.value); } } \ No newline at end of file From 034db372e1737e7b0f8830ca55b2f7baa1a3b952 Mon Sep 17 00:00:00 2001 From: Andrii Kostenko Date: Sat, 28 Jun 2025 20:51:22 +0000 Subject: [PATCH 03/10] correct attr --- frontend/package.json | 2 +- .../row-fields/phone/phone.component.html | 6 +- frontend/yarn.lock | 68 +++++-------------- 3 files changed, 21 insertions(+), 55 deletions(-) diff --git a/frontend/package.json b/frontend/package.json index 49e239601..2846f576f 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -44,8 +44,8 @@ "lodash": "^4.17.21", "ng-dynamic-component": "^10.7.0", "ngx-cookie-service": "^19.0.0", - "ngx-mat-input-tel": "^9.0.0", "ngx-markdown": "^19.0.0", + "ngx-mat-input-tel": "^19.0.0", "ngx-stripe": "^19.0.0", "pluralize": "^8.0.0", "postgres-interval": "^4.0.2", diff --git a/frontend/src/app/components/ui-components/row-fields/phone/phone.component.html b/frontend/src/app/components/ui-components/row-fields/phone/phone.component.html index 2a735e421..c0d58cada 100644 --- a/frontend/src/app/components/ui-components/row-fields/phone/phone.component.html +++ b/frontend/src/app/components/ui-components/row-fields/phone/phone.component.html @@ -2,13 +2,11 @@ {{normalizedLabel}} + [(ngModel)]="value" + (ngModelChange)="onPhoneNumberChange($event)"> \ No newline at end of file diff --git a/frontend/yarn.lock b/frontend/yarn.lock index 6cc2acbc9..e847c27ae 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -6243,8 +6243,6 @@ __metadata: angular-password-strength-meter: ^12.0.0 angulartics2: ^14.1.0 date-fns: ^4.1.0 - google-libphonenumber: ^3.2.42 - intl-tel-input: ^25.3.1 ipaddr.js: ^2.2.0 jasmine-core: ~5.5.0 jasmine-spec-reporter: ~7.0.0 @@ -6257,10 +6255,9 @@ __metadata: karma-jasmine-html-reporter: ^2.1.0 lodash: ^4.17.21 ng-dynamic-component: ^10.7.0 - ngx-bootstrap: ^19.0.2 ngx-cookie-service: ^19.0.0 - ngx-intl-tel-input: ^17.0.0 ngx-markdown: ^19.0.0 + ngx-mat-input-tel: ^19.0.0 ngx-stripe: ^19.0.0 pluralize: ^8.0.0 postgres-interval: ^4.0.2 @@ -7358,13 +7355,6 @@ __metadata: languageName: node linkType: hard -"google-libphonenumber@npm:^3.2.42": - version: 3.2.42 - resolution: "google-libphonenumber@npm:3.2.42" - checksum: f98ea03d7509ccef535a6490089895ae2ae03b49c8c3a3c604d94184064379442357c58675759f12f70be8c6ea8c224799ea751830971fb54a116c3751241657 - languageName: node - linkType: hard - "gopd@npm:^1.2.0": version: 1.2.0 resolution: "gopd@npm:1.2.0" @@ -7733,13 +7723,6 @@ __metadata: languageName: node linkType: hard -"intl-tel-input@npm:^25.3.1": - version: 25.3.1 - resolution: "intl-tel-input@npm:25.3.1" - checksum: f4bb9cd2cfe8370ab6f5c4836747bd22a301ed9cc2c4987b0760135e1b0101ab87c5093796b789e09fab3c6e787bda12dbfb015bdade239809447dcaeab91aae - languageName: node - linkType: hard - "ip-address@npm:^9.0.5": version: 9.0.5 resolution: "ip-address@npm:9.0.5" @@ -9229,21 +9212,6 @@ __metadata: languageName: node linkType: hard -"ngx-bootstrap@npm:^19.0.2": - version: 19.0.2 - resolution: "ngx-bootstrap@npm:19.0.2" - dependencies: - tslib: ^2.3.0 - peerDependencies: - "@angular/animations": ^19.0.1 - "@angular/common": ^19.0.1 - "@angular/core": ^19.0.1 - "@angular/forms": ^19.0.1 - rxjs: ^6.5.3 || ^7.4.0 - checksum: d919e82d827fb2171e36e8a62a631ebe2113c162af325f7e43f38295e3ce84c83b6d8d34a7131393150b2343619309ae33bece0ca7ae75a6631797bf2a8a3429 - languageName: node - linkType: hard - "ngx-cookie-service@npm:^19.0.0": version: 19.0.0 resolution: "ngx-cookie-service@npm:19.0.0" @@ -9256,22 +9224,6 @@ __metadata: languageName: node linkType: hard -"ngx-intl-tel-input@npm:^17.0.0": - version: 17.0.0 - resolution: "ngx-intl-tel-input@npm:17.0.0" - dependencies: - tslib: ^2.3.0 - peerDependencies: - "@angular/common": ">= 17.0.0" - "@angular/core": ">= 17.0.0" - "@angular/forms": ">= 17.0.0" - google-libphonenumber: ^3.0.0 - intl-tel-input: ^19.0.0 - ngx-bootstrap: ">= 12.0.0" - checksum: 8d1a7d8545118c8e79e0f073c75a73a5fead30c64df45831f0e27cc78d6d8c8042490893f273ff65611e5b4d5d07b5b85f7c8a823b1463474080dd0a6b27084a - languageName: node - linkType: hard - "ngx-markdown@npm:^19.0.0": version: 19.0.0 resolution: "ngx-markdown@npm:19.0.0" @@ -9304,6 +9256,22 @@ __metadata: languageName: node linkType: hard +"ngx-mat-input-tel@npm:^19.0.0": + version: 19.5.1 + resolution: "ngx-mat-input-tel@npm:19.5.1" + dependencies: + tslib: ^2.x + peerDependencies: + "@angular/common": ">=19.x" + "@angular/core": ">=19.x" + "@angular/forms": ">=19.x" + "@angular/platform-browser": ">=19.x" + "@angular/platform-browser-dynamic": ">=19.x" + libphonenumber-js: ^1.12.5 + checksum: 1cd9d15f5cd7cbec0e528707b9dfc7e2bfc8a2ad32fa1ad551559b9367f283e2217d188bade72f213d13f9efc90ee45316d9f167193f137850b7a5e6890cdf27 + languageName: node + linkType: hard + "ngx-stripe@npm:^19.0.0": version: 19.0.0 resolution: "ngx-stripe@npm:19.0.0" @@ -11906,7 +11874,7 @@ __metadata: languageName: node linkType: hard -"tslib@npm:2.8.1, tslib@npm:^2.0.0, tslib@npm:^2.0.1, tslib@npm:^2.1.0, tslib@npm:^2.3.0, tslib@npm:^2.4.1, tslib@npm:^2.5.0, tslib@npm:^2.7.0, tslib@npm:^2.8.0, tslib@npm:^2.8.1": +"tslib@npm:2.8.1, tslib@npm:^2.0.0, tslib@npm:^2.0.1, tslib@npm:^2.1.0, tslib@npm:^2.3.0, tslib@npm:^2.4.1, tslib@npm:^2.5.0, tslib@npm:^2.7.0, tslib@npm:^2.8.0, tslib@npm:^2.8.1, tslib@npm:^2.x": version: 2.8.1 resolution: "tslib@npm:2.8.1" checksum: e4aba30e632b8c8902b47587fd13345e2827fa639e7c3121074d5ee0880723282411a8838f830b55100cbe4517672f84a2472667d355b81e8af165a55dc6203a From dced82be5e0b481f6e7b580a3e4a862afc910369 Mon Sep 17 00:00:00 2001 From: Andrii Kostenko Date: Mon, 30 Jun 2025 11:35:42 +0000 Subject: [PATCH 04/10] set packagemanager field --- frontend/package.json | 3 ++- package.json | 5 ++++- yarn.lock | 9 +++++++++ 3 files changed, 15 insertions(+), 2 deletions(-) diff --git a/frontend/package.json b/frontend/package.json index 2846f576f..c68fc5d59 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -75,5 +75,6 @@ "karma-jasmine-html-reporter": "^2.1.0", "ts-node": "~10.9.2", "typescript": "~5.6.0" - } + }, + "packageManager": "yarn@1.22.22" } diff --git a/package.json b/package.json index 0189c8da2..ba6fcf0fa 100644 --- a/package.json +++ b/package.json @@ -6,5 +6,8 @@ "rocketadmin-agent", "shared-code" ], - "packageManager": "yarn@3.4.1" + "packageManager": "yarn@3.4.1", + "dependencies": { + "libphonenumber-js": "^1.12.9" + } } diff --git a/yarn.lock b/yarn.lock index b259948a8..81e6b7c40 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9921,6 +9921,13 @@ __metadata: languageName: node linkType: hard +"libphonenumber-js@npm:^1.12.9": + version: 1.12.9 + resolution: "libphonenumber-js@npm:1.12.9" + checksum: 9d01151ffa1d0f634ebbc4e7d5cde6baa7c53765e162745cb2dd85815e04b42847ff2e4c54fe172121c335c86cd6f5ce9192af62237099213e799de653d5a6dd + languageName: node + linkType: hard + "lines-and-columns@npm:^1.1.6": version: 1.2.4 resolution: "lines-and-columns@npm:1.2.4" @@ -12258,6 +12265,8 @@ __metadata: "root@workspace:.": version: 0.0.0-use.local resolution: "root@workspace:." + dependencies: + libphonenumber-js: ^1.12.9 languageName: unknown linkType: soft From 48a577efd8d57373026029a2734903ade82511df Mon Sep 17 00:00:00 2001 From: Andrii Kostenko Date: Mon, 30 Jun 2025 11:48:01 +0000 Subject: [PATCH 05/10] missing deps --- frontend/package.json | 1 + frontend/yarn.lock | 8 ++++++++ 2 files changed, 9 insertions(+) diff --git a/frontend/package.json b/frontend/package.json index c68fc5d59..fce36c6e6 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -41,6 +41,7 @@ "date-fns": "^4.1.0", "ipaddr.js": "^2.2.0", "json5": "^2.2.3", + "libphonenumber-js": "^1.12.9", "lodash": "^4.17.21", "ng-dynamic-component": "^10.7.0", "ngx-cookie-service": "^19.0.0", diff --git a/frontend/yarn.lock b/frontend/yarn.lock index e847c27ae..4a68616e6 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -6253,6 +6253,7 @@ __metadata: karma-coverage-istanbul-reporter: ^3.0.3 karma-jasmine: ~5.1.0 karma-jasmine-html-reporter: ^2.1.0 + libphonenumber-js: ^1.12.9 lodash: ^4.17.21 ng-dynamic-component: ^10.7.0 ngx-cookie-service: ^19.0.0 @@ -8427,6 +8428,13 @@ __metadata: languageName: node linkType: hard +"libphonenumber-js@npm:^1.12.9": + version: 1.12.9 + resolution: "libphonenumber-js@npm:1.12.9" + checksum: 9d01151ffa1d0f634ebbc4e7d5cde6baa7c53765e162745cb2dd85815e04b42847ff2e4c54fe172121c335c86cd6f5ce9192af62237099213e799de653d5a6dd + languageName: node + linkType: hard + "license-webpack-plugin@npm:4.0.2": version: 4.0.2 resolution: "license-webpack-plugin@npm:4.0.2" From ccade2d465eb1075ee5b4037b1aa6b7ce6c8a49e Mon Sep 17 00:00:00 2001 From: Andrii Kostenko Date: Tue, 1 Jul 2025 12:22:30 +0000 Subject: [PATCH 06/10] no deps implementation --- frontend/package.json | 2 - .../row-fields/phone/phone.component.css | 74 +++- .../row-fields/phone/phone.component.html | 53 ++- .../row-fields/phone/phone.component.ts | 413 +++++++++++++++++- frontend/yarn.lock | 27 +- 5 files changed, 503 insertions(+), 66 deletions(-) diff --git a/frontend/package.json b/frontend/package.json index fce36c6e6..5afd1cd56 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -41,12 +41,10 @@ "date-fns": "^4.1.0", "ipaddr.js": "^2.2.0", "json5": "^2.2.3", - "libphonenumber-js": "^1.12.9", "lodash": "^4.17.21", "ng-dynamic-component": "^10.7.0", "ngx-cookie-service": "^19.0.0", "ngx-markdown": "^19.0.0", - "ngx-mat-input-tel": "^19.0.0", "ngx-stripe": "^19.0.0", "pluralize": "^8.0.0", "postgres-interval": "^4.0.2", diff --git a/frontend/src/app/components/ui-components/row-fields/phone/phone.component.css b/frontend/src/app/components/ui-components/row-fields/phone/phone.component.css index 593dcec59..88291a68b 100644 --- a/frontend/src/app/components/ui-components/row-fields/phone/phone.component.css +++ b/frontend/src/app/components/ui-components/row-fields/phone/phone.component.css @@ -1,23 +1,67 @@ -.phone-form-field { +.phone-input-container { + display: flex; + gap: 12px; + align-items: flex-start; width: 100%; } -.phone-form-field ::ng-deep .ngx-int-tel-input { - width: 100%; +.country-select-field { + flex: 0 0 200px; + min-width: 200px; +} + +.phone-number-field { + flex: 1; + min-width: 0; +} + +.country-option { + display: flex; + align-items: center; + gap: 8px; +} + +.flag { + font-size: 16px; + width: 20px; + text-align: center; +} + +.dial-code { + font-weight: 500; + color: rgba(0, 0, 0, 0.87); + min-width: 40px; +} + +.country-name { + color: rgba(0, 0, 0, 0.6); +} + +.phone-number-field input.invalid { + border-color: #f44336; +} + +@media (max-width: 768px) { + .phone-input-container { + flex-direction: column; + gap: 8px; + } + + .country-select-field { + flex: none; + width: 100%; + } + + .phone-number-field { + width: 100%; + } } -.phone-form-field ::ng-deep .ngx-int-tel-input .form-control { - border: none; - outline: none; - box-shadow: none; - padding: 0; - font-size: inherit; - font-family: inherit; - color: inherit; - background: transparent; +/* Dark theme support */ +:host-context(.dark-theme) .dial-code { + color: rgba(255, 255, 255, 0.87); } -.phone-form-field ::ng-deep .ngx-int-tel-input .country-dropdown { - border: none; - background: transparent; +:host-context(.dark-theme) .country-name { + color: rgba(255, 255, 255, 0.6); } \ No newline at end of file diff --git a/frontend/src/app/components/ui-components/row-fields/phone/phone.component.html b/frontend/src/app/components/ui-components/row-fields/phone/phone.component.html index c0d58cada..56b31e28e 100644 --- a/frontend/src/app/components/ui-components/row-fields/phone/phone.component.html +++ b/frontend/src/app/components/ui-components/row-fields/phone/phone.component.html @@ -1,12 +1,43 @@ - +
+ + Country + + + + {{selectedCountry.flag}} + {{selectedCountry.dialCode}} + {{selectedCountry.name}} + + + + + {{country.flag}} + {{country.dialCode}} + {{country.name}} + + + + + + {{normalizedLabel}} - - - \ No newline at end of file + + {{selectedCountry.dialCode}} + Enter international number (e.g., +1234567890) or select country first + + Invalid phone number format + + +
\ No newline at end of file diff --git a/frontend/src/app/components/ui-components/row-fields/phone/phone.component.ts b/frontend/src/app/components/ui-components/row-fields/phone/phone.component.ts index 352038906..9dd8884b3 100644 --- a/frontend/src/app/components/ui-components/row-fields/phone/phone.component.ts +++ b/frontend/src/app/components/ui-components/row-fields/phone/phone.component.ts @@ -1,9 +1,17 @@ -import { Component, Injectable, Input, OnInit, AfterViewInit } from '@angular/core'; -import { NgxMatInputTelComponent } from 'ngx-mat-input-tel'; - +import { Component, Injectable, Input, OnInit } from '@angular/core'; import { BaseRowFieldComponent } from '../base-row-field/base-row-field.component'; import { FormsModule } from '@angular/forms'; import { MatFormFieldModule } from '@angular/material/form-field'; +import { MatInputModule } from '@angular/material/input'; +import { MatSelectModule } from '@angular/material/select'; +import { CommonModule } from '@angular/common'; + +interface CountryCode { + code: string; + name: string; + dialCode: string; + flag: string; +} @Injectable() @@ -11,10 +19,10 @@ import { MatFormFieldModule } from '@angular/material/form-field'; selector: 'app-row-phone', templateUrl: './phone.component.html', styleUrls: ['./phone.component.css'], - imports: [MatFormFieldModule, FormsModule, NgxMatInputTelComponent] + imports: [CommonModule, MatFormFieldModule, MatInputModule, MatSelectModule, FormsModule] }) -export class PhoneRowComponent extends BaseRowFieldComponent implements OnInit, AfterViewInit { - @Input() value: string; +export class PhoneRowComponent extends BaseRowFieldComponent implements OnInit { + @Input() value: string = ''; static type = 'phone'; @@ -23,13 +31,260 @@ export class PhoneRowComponent extends BaseRowFieldComponent implements OnInit, enableAutoCountrySelect: boolean = true; phoneValidation: boolean = true; + selectedCountry: CountryCode; + phoneNumber: string = ''; + + countries: CountryCode[] = [ + { code: 'AF', name: 'Afghanistan', dialCode: '+93', flag: '๐Ÿ‡ฆ๐Ÿ‡ซ' }, + { code: 'AL', name: 'Albania', dialCode: '+355', flag: '๐Ÿ‡ฆ๐Ÿ‡ฑ' }, + { code: 'DZ', name: 'Algeria', dialCode: '+213', flag: '๐Ÿ‡ฉ๐Ÿ‡ฟ' }, + { code: 'AS', name: 'American Samoa', dialCode: '+1684', flag: '๐Ÿ‡ฆ๐Ÿ‡ธ' }, + { code: 'AD', name: 'Andorra', dialCode: '+376', flag: '๐Ÿ‡ฆ๐Ÿ‡ฉ' }, + { code: 'AO', name: 'Angola', dialCode: '+244', flag: '๐Ÿ‡ฆ๐Ÿ‡ด' }, + { code: 'AI', name: 'Anguilla', dialCode: '+1264', flag: '๐Ÿ‡ฆ๐Ÿ‡ฎ' }, + { code: 'AQ', name: 'Antarctica', dialCode: '+672', flag: '๐Ÿ‡ฆ๐Ÿ‡ถ' }, + { code: 'AG', name: 'Antigua and Barbuda', dialCode: '+1268', flag: '๐Ÿ‡ฆ๐Ÿ‡ฌ' }, + { code: 'AR', name: 'Argentina', dialCode: '+54', flag: '๐Ÿ‡ฆ๐Ÿ‡ท' }, + { code: 'AM', name: 'Armenia', dialCode: '+374', flag: '๐Ÿ‡ฆ๐Ÿ‡ฒ' }, + { code: 'AW', name: 'Aruba', dialCode: '+297', flag: '๐Ÿ‡ฆ๐Ÿ‡ผ' }, + { code: 'AU', name: 'Australia', dialCode: '+61', flag: '๐Ÿ‡ฆ๐Ÿ‡บ' }, + { code: 'AT', name: 'Austria', dialCode: '+43', flag: '๐Ÿ‡ฆ๐Ÿ‡น' }, + { code: 'AZ', name: 'Azerbaijan', dialCode: '+994', flag: '๐Ÿ‡ฆ๐Ÿ‡ฟ' }, + { code: 'BS', name: 'Bahamas', dialCode: '+1242', flag: '๐Ÿ‡ง๐Ÿ‡ธ' }, + { code: 'BH', name: 'Bahrain', dialCode: '+973', flag: '๐Ÿ‡ง๐Ÿ‡ญ' }, + { code: 'BD', name: 'Bangladesh', dialCode: '+880', flag: '๐Ÿ‡ง๐Ÿ‡ฉ' }, + { code: 'BB', name: 'Barbados', dialCode: '+1246', flag: '๐Ÿ‡ง๐Ÿ‡ง' }, + { code: 'BY', name: 'Belarus', dialCode: '+375', flag: '๐Ÿ‡ง๐Ÿ‡พ' }, + { code: 'BE', name: 'Belgium', dialCode: '+32', flag: '๐Ÿ‡ง๐Ÿ‡ช' }, + { code: 'BZ', name: 'Belize', dialCode: '+501', flag: '๐Ÿ‡ง๐Ÿ‡ฟ' }, + { code: 'BJ', name: 'Benin', dialCode: '+229', flag: '๐Ÿ‡ง๐Ÿ‡ฏ' }, + { code: 'BM', name: 'Bermuda', dialCode: '+1441', flag: '๐Ÿ‡ง๐Ÿ‡ฒ' }, + { code: 'BT', name: 'Bhutan', dialCode: '+975', flag: '๐Ÿ‡ง๐Ÿ‡น' }, + { code: 'BO', name: 'Bolivia', dialCode: '+591', flag: '๐Ÿ‡ง๐Ÿ‡ด' }, + { code: 'BA', name: 'Bosnia and Herzegovina', dialCode: '+387', flag: '๐Ÿ‡ง๐Ÿ‡ฆ' }, + { code: 'BW', name: 'Botswana', dialCode: '+267', flag: '๐Ÿ‡ง๐Ÿ‡ผ' }, + { code: 'BR', name: 'Brazil', dialCode: '+55', flag: '๐Ÿ‡ง๐Ÿ‡ท' }, + { code: 'IO', name: 'British Indian Ocean Territory', dialCode: '+246', flag: '๐Ÿ‡ฎ๐Ÿ‡ด' }, + { code: 'BN', name: 'Brunei', dialCode: '+673', flag: '๐Ÿ‡ง๐Ÿ‡ณ' }, + { code: 'BG', name: 'Bulgaria', dialCode: '+359', flag: '๐Ÿ‡ง๐Ÿ‡ฌ' }, + { code: 'BF', name: 'Burkina Faso', dialCode: '+226', flag: '๐Ÿ‡ง๐Ÿ‡ซ' }, + { code: 'BI', name: 'Burundi', dialCode: '+257', flag: '๐Ÿ‡ง๐Ÿ‡ฎ' }, + { code: 'KH', name: 'Cambodia', dialCode: '+855', flag: '๐Ÿ‡ฐ๐Ÿ‡ญ' }, + { code: 'CM', name: 'Cameroon', dialCode: '+237', flag: '๐Ÿ‡จ๐Ÿ‡ฒ' }, + { code: 'CA', name: 'Canada', dialCode: '+1', flag: '๐Ÿ‡จ๐Ÿ‡ฆ' }, + { code: 'CV', name: 'Cape Verde', dialCode: '+238', flag: '๐Ÿ‡จ๐Ÿ‡ป' }, + { code: 'KY', name: 'Cayman Islands', dialCode: '+1345', flag: '๐Ÿ‡ฐ๐Ÿ‡พ' }, + { code: 'CF', name: 'Central African Republic', dialCode: '+236', flag: '๐Ÿ‡จ๐Ÿ‡ซ' }, + { code: 'TD', name: 'Chad', dialCode: '+235', flag: '๐Ÿ‡น๐Ÿ‡ฉ' }, + { code: 'CL', name: 'Chile', dialCode: '+56', flag: '๐Ÿ‡จ๐Ÿ‡ฑ' }, + { code: 'CN', name: 'China', dialCode: '+86', flag: '๐Ÿ‡จ๐Ÿ‡ณ' }, + { code: 'CX', name: 'Christmas Island', dialCode: '+61', flag: '๐Ÿ‡จ๐Ÿ‡ฝ' }, + { code: 'CC', name: 'Cocos Islands', dialCode: '+61', flag: '๐Ÿ‡จ๐Ÿ‡จ' }, + { code: 'CO', name: 'Colombia', dialCode: '+57', flag: '๐Ÿ‡จ๐Ÿ‡ด' }, + { code: 'KM', name: 'Comoros', dialCode: '+269', flag: '๐Ÿ‡ฐ๐Ÿ‡ฒ' }, + { code: 'CG', name: 'Congo', dialCode: '+242', flag: '๐Ÿ‡จ๐Ÿ‡ฌ' }, + { code: 'CD', name: 'Congo (DRC)', dialCode: '+243', flag: '๐Ÿ‡จ๐Ÿ‡ฉ' }, + { code: 'CK', name: 'Cook Islands', dialCode: '+682', flag: '๐Ÿ‡จ๐Ÿ‡ฐ' }, + { code: 'CR', name: 'Costa Rica', dialCode: '+506', flag: '๐Ÿ‡จ๐Ÿ‡ท' }, + { code: 'CI', name: 'Cรดte d\'Ivoire', dialCode: '+225', flag: '๐Ÿ‡จ๐Ÿ‡ฎ' }, + { code: 'HR', name: 'Croatia', dialCode: '+385', flag: '๐Ÿ‡ญ๐Ÿ‡ท' }, + { code: 'CU', name: 'Cuba', dialCode: '+53', flag: '๐Ÿ‡จ๐Ÿ‡บ' }, + { code: 'CW', name: 'Curaรงao', dialCode: '+599', flag: '๐Ÿ‡จ๐Ÿ‡ผ' }, + { code: 'CY', name: 'Cyprus', dialCode: '+357', flag: '๐Ÿ‡จ๐Ÿ‡พ' }, + { code: 'CZ', name: 'Czech Republic', dialCode: '+420', flag: '๐Ÿ‡จ๐Ÿ‡ฟ' }, + { code: 'DK', name: 'Denmark', dialCode: '+45', flag: '๐Ÿ‡ฉ๐Ÿ‡ฐ' }, + { code: 'DJ', name: 'Djibouti', dialCode: '+253', flag: '๐Ÿ‡ฉ๐Ÿ‡ฏ' }, + { code: 'DM', name: 'Dominica', dialCode: '+1767', flag: '๐Ÿ‡ฉ๐Ÿ‡ฒ' }, + { code: 'DO', name: 'Dominican Republic', dialCode: '+1', flag: '๐Ÿ‡ฉ๐Ÿ‡ด' }, + { code: 'EC', name: 'Ecuador', dialCode: '+593', flag: '๐Ÿ‡ช๐Ÿ‡จ' }, + { code: 'EG', name: 'Egypt', dialCode: '+20', flag: '๐Ÿ‡ช๐Ÿ‡ฌ' }, + { code: 'SV', name: 'El Salvador', dialCode: '+503', flag: '๐Ÿ‡ธ๐Ÿ‡ป' }, + { code: 'GQ', name: 'Equatorial Guinea', dialCode: '+240', flag: '๐Ÿ‡ฌ๐Ÿ‡ถ' }, + { code: 'ER', name: 'Eritrea', dialCode: '+291', flag: '๐Ÿ‡ช๐Ÿ‡ท' }, + { code: 'EE', name: 'Estonia', dialCode: '+372', flag: '๐Ÿ‡ช๐Ÿ‡ช' }, + { code: 'ET', name: 'Ethiopia', dialCode: '+251', flag: '๐Ÿ‡ช๐Ÿ‡น' }, + { code: 'FK', name: 'Falkland Islands', dialCode: '+500', flag: '๐Ÿ‡ซ๐Ÿ‡ฐ' }, + { code: 'FO', name: 'Faroe Islands', dialCode: '+298', flag: '๐Ÿ‡ซ๐Ÿ‡ด' }, + { code: 'FJ', name: 'Fiji', dialCode: '+679', flag: '๐Ÿ‡ซ๐Ÿ‡ฏ' }, + { code: 'FI', name: 'Finland', dialCode: '+358', flag: '๐Ÿ‡ซ๐Ÿ‡ฎ' }, + { code: 'FR', name: 'France', dialCode: '+33', flag: '๐Ÿ‡ซ๐Ÿ‡ท' }, + { code: 'GF', name: 'French Guiana', dialCode: '+594', flag: '๐Ÿ‡ฌ๐Ÿ‡ซ' }, + { code: 'PF', name: 'French Polynesia', dialCode: '+689', flag: '๐Ÿ‡ต๐Ÿ‡ซ' }, + { code: 'GA', name: 'Gabon', dialCode: '+241', flag: '๐Ÿ‡ฌ๐Ÿ‡ฆ' }, + { code: 'GM', name: 'Gambia', dialCode: '+220', flag: '๐Ÿ‡ฌ๐Ÿ‡ฒ' }, + { code: 'GE', name: 'Georgia', dialCode: '+995', flag: '๐Ÿ‡ฌ๐Ÿ‡ช' }, + { code: 'DE', name: 'Germany', dialCode: '+49', flag: '๐Ÿ‡ฉ๐Ÿ‡ช' }, + { code: 'GH', name: 'Ghana', dialCode: '+233', flag: '๐Ÿ‡ฌ๐Ÿ‡ญ' }, + { code: 'GI', name: 'Gibraltar', dialCode: '+350', flag: '๐Ÿ‡ฌ๐Ÿ‡ฎ' }, + { code: 'GR', name: 'Greece', dialCode: '+30', flag: '๐Ÿ‡ฌ๐Ÿ‡ท' }, + { code: 'GL', name: 'Greenland', dialCode: '+299', flag: '๐Ÿ‡ฌ๐Ÿ‡ฑ' }, + { code: 'GD', name: 'Grenada', dialCode: '+1473', flag: '๐Ÿ‡ฌ๐Ÿ‡ฉ' }, + { code: 'GP', name: 'Guadeloupe', dialCode: '+590', flag: '๐Ÿ‡ฌ๐Ÿ‡ต' }, + { code: 'GU', name: 'Guam', dialCode: '+1671', flag: '๐Ÿ‡ฌ๐Ÿ‡บ' }, + { code: 'GT', name: 'Guatemala', dialCode: '+502', flag: '๐Ÿ‡ฌ๐Ÿ‡น' }, + { code: 'GG', name: 'Guernsey', dialCode: '+44', flag: '๐Ÿ‡ฌ๐Ÿ‡ฌ' }, + { code: 'GN', name: 'Guinea', dialCode: '+224', flag: '๐Ÿ‡ฌ๐Ÿ‡ณ' }, + { code: 'GW', name: 'Guinea-Bissau', dialCode: '+245', flag: '๐Ÿ‡ฌ๐Ÿ‡ผ' }, + { code: 'GY', name: 'Guyana', dialCode: '+592', flag: '๐Ÿ‡ฌ๐Ÿ‡พ' }, + { code: 'HT', name: 'Haiti', dialCode: '+509', flag: '๐Ÿ‡ญ๐Ÿ‡น' }, + { code: 'VA', name: 'Holy See', dialCode: '+379', flag: '๐Ÿ‡ป๐Ÿ‡ฆ' }, + { code: 'HN', name: 'Honduras', dialCode: '+504', flag: '๐Ÿ‡ญ๐Ÿ‡ณ' }, + { code: 'HK', name: 'Hong Kong', dialCode: '+852', flag: '๐Ÿ‡ญ๐Ÿ‡ฐ' }, + { code: 'HU', name: 'Hungary', dialCode: '+36', flag: '๐Ÿ‡ญ๐Ÿ‡บ' }, + { code: 'IS', name: 'Iceland', dialCode: '+354', flag: '๐Ÿ‡ฎ๐Ÿ‡ธ' }, + { code: 'IN', name: 'India', dialCode: '+91', flag: '๐Ÿ‡ฎ๐Ÿ‡ณ' }, + { code: 'ID', name: 'Indonesia', dialCode: '+62', flag: '๐Ÿ‡ฎ๐Ÿ‡ฉ' }, + { code: 'IR', name: 'Iran', dialCode: '+98', flag: '๐Ÿ‡ฎ๐Ÿ‡ท' }, + { code: 'IQ', name: 'Iraq', dialCode: '+964', flag: '๐Ÿ‡ฎ๐Ÿ‡ถ' }, + { code: 'IE', name: 'Ireland', dialCode: '+353', flag: '๐Ÿ‡ฎ๐Ÿ‡ช' }, + { code: 'IM', name: 'Isle of Man', dialCode: '+44', flag: '๐Ÿ‡ฎ๐Ÿ‡ฒ' }, + { code: 'IL', name: 'Israel', dialCode: '+972', flag: '๐Ÿ‡ฎ๐Ÿ‡ฑ' }, + { code: 'IT', name: 'Italy', dialCode: '+39', flag: '๐Ÿ‡ฎ๐Ÿ‡น' }, + { code: 'JM', name: 'Jamaica', dialCode: '+1876', flag: '๐Ÿ‡ฏ๐Ÿ‡ฒ' }, + { code: 'JP', name: 'Japan', dialCode: '+81', flag: '๐Ÿ‡ฏ๐Ÿ‡ต' }, + { code: 'JE', name: 'Jersey', dialCode: '+44', flag: '๐Ÿ‡ฏ๐Ÿ‡ช' }, + { code: 'JO', name: 'Jordan', dialCode: '+962', flag: '๐Ÿ‡ฏ๐Ÿ‡ด' }, + { code: 'KZ', name: 'Kazakhstan', dialCode: '+7', flag: '๐Ÿ‡ฐ๐Ÿ‡ฟ' }, + { code: 'KE', name: 'Kenya', dialCode: '+254', flag: '๐Ÿ‡ฐ๐Ÿ‡ช' }, + { code: 'KI', name: 'Kiribati', dialCode: '+686', flag: '๐Ÿ‡ฐ๐Ÿ‡ฎ' }, + { code: 'KP', name: 'North Korea', dialCode: '+850', flag: '๐Ÿ‡ฐ๐Ÿ‡ต' }, + { code: 'KR', name: 'South Korea', dialCode: '+82', flag: '๐Ÿ‡ฐ๐Ÿ‡ท' }, + { code: 'KW', name: 'Kuwait', dialCode: '+965', flag: '๐Ÿ‡ฐ๐Ÿ‡ผ' }, + { code: 'KG', name: 'Kyrgyzstan', dialCode: '+996', flag: '๐Ÿ‡ฐ๐Ÿ‡ฌ' }, + { code: 'LA', name: 'Laos', dialCode: '+856', flag: '๐Ÿ‡ฑ๐Ÿ‡ฆ' }, + { code: 'LV', name: 'Latvia', dialCode: '+371', flag: '๐Ÿ‡ฑ๐Ÿ‡ป' }, + { code: 'LB', name: 'Lebanon', dialCode: '+961', flag: '๐Ÿ‡ฑ๐Ÿ‡ง' }, + { code: 'LS', name: 'Lesotho', dialCode: '+266', flag: '๐Ÿ‡ฑ๐Ÿ‡ธ' }, + { code: 'LR', name: 'Liberia', dialCode: '+231', flag: '๐Ÿ‡ฑ๐Ÿ‡ท' }, + { code: 'LY', name: 'Libya', dialCode: '+218', flag: '๐Ÿ‡ฑ๐Ÿ‡พ' }, + { code: 'LI', name: 'Liechtenstein', dialCode: '+423', flag: '๐Ÿ‡ฑ๐Ÿ‡ฎ' }, + { code: 'LT', name: 'Lithuania', dialCode: '+370', flag: '๐Ÿ‡ฑ๐Ÿ‡น' }, + { code: 'LU', name: 'Luxembourg', dialCode: '+352', flag: '๐Ÿ‡ฑ๐Ÿ‡บ' }, + { code: 'MO', name: 'Macau', dialCode: '+853', flag: '๐Ÿ‡ฒ๐Ÿ‡ด' }, + { code: 'MK', name: 'North Macedonia', dialCode: '+389', flag: '๐Ÿ‡ฒ๐Ÿ‡ฐ' }, + { code: 'MG', name: 'Madagascar', dialCode: '+261', flag: '๐Ÿ‡ฒ๐Ÿ‡ฌ' }, + { code: 'MW', name: 'Malawi', dialCode: '+265', flag: '๐Ÿ‡ฒ๐Ÿ‡ผ' }, + { code: 'MY', name: 'Malaysia', dialCode: '+60', flag: '๐Ÿ‡ฒ๐Ÿ‡พ' }, + { code: 'MV', name: 'Maldives', dialCode: '+960', flag: '๐Ÿ‡ฒ๐Ÿ‡ป' }, + { code: 'ML', name: 'Mali', dialCode: '+223', flag: '๐Ÿ‡ฒ๐Ÿ‡ฑ' }, + { code: 'MT', name: 'Malta', dialCode: '+356', flag: '๐Ÿ‡ฒ๐Ÿ‡น' }, + { code: 'MH', name: 'Marshall Islands', dialCode: '+692', flag: '๐Ÿ‡ฒ๐Ÿ‡ญ' }, + { code: 'MQ', name: 'Martinique', dialCode: '+596', flag: '๐Ÿ‡ฒ๐Ÿ‡ถ' }, + { code: 'MR', name: 'Mauritania', dialCode: '+222', flag: '๐Ÿ‡ฒ๐Ÿ‡ท' }, + { code: 'MU', name: 'Mauritius', dialCode: '+230', flag: '๐Ÿ‡ฒ๐Ÿ‡บ' }, + { code: 'YT', name: 'Mayotte', dialCode: '+262', flag: '๐Ÿ‡พ๐Ÿ‡น' }, + { code: 'MX', name: 'Mexico', dialCode: '+52', flag: '๐Ÿ‡ฒ๐Ÿ‡ฝ' }, + { code: 'FM', name: 'Micronesia', dialCode: '+691', flag: '๐Ÿ‡ซ๐Ÿ‡ฒ' }, + { code: 'MD', name: 'Moldova', dialCode: '+373', flag: '๐Ÿ‡ฒ๐Ÿ‡ฉ' }, + { code: 'MC', name: 'Monaco', dialCode: '+377', flag: '๐Ÿ‡ฒ๐Ÿ‡จ' }, + { code: 'MN', name: 'Mongolia', dialCode: '+976', flag: '๐Ÿ‡ฒ๐Ÿ‡ณ' }, + { code: 'ME', name: 'Montenegro', dialCode: '+382', flag: '๐Ÿ‡ฒ๐Ÿ‡ช' }, + { code: 'MS', name: 'Montserrat', dialCode: '+1664', flag: '๐Ÿ‡ฒ๐Ÿ‡ธ' }, + { code: 'MA', name: 'Morocco', dialCode: '+212', flag: '๐Ÿ‡ฒ๐Ÿ‡ฆ' }, + { code: 'MZ', name: 'Mozambique', dialCode: '+258', flag: '๐Ÿ‡ฒ๐Ÿ‡ฟ' }, + { code: 'MM', name: 'Myanmar', dialCode: '+95', flag: '๐Ÿ‡ฒ๐Ÿ‡ฒ' }, + { code: 'NA', name: 'Namibia', dialCode: '+264', flag: '๐Ÿ‡ณ๐Ÿ‡ฆ' }, + { code: 'NR', name: 'Nauru', dialCode: '+674', flag: '๐Ÿ‡ณ๐Ÿ‡ท' }, + { code: 'NP', name: 'Nepal', dialCode: '+977', flag: '๐Ÿ‡ณ๐Ÿ‡ต' }, + { code: 'NL', name: 'Netherlands', dialCode: '+31', flag: '๐Ÿ‡ณ๐Ÿ‡ฑ' }, + { code: 'NC', name: 'New Caledonia', dialCode: '+687', flag: '๐Ÿ‡ณ๐Ÿ‡จ' }, + { code: 'NZ', name: 'New Zealand', dialCode: '+64', flag: '๐Ÿ‡ณ๐Ÿ‡ฟ' }, + { code: 'NI', name: 'Nicaragua', dialCode: '+505', flag: '๐Ÿ‡ณ๐Ÿ‡ฎ' }, + { code: 'NE', name: 'Niger', dialCode: '+227', flag: '๐Ÿ‡ณ๐Ÿ‡ช' }, + { code: 'NG', name: 'Nigeria', dialCode: '+234', flag: '๐Ÿ‡ณ๐Ÿ‡ฌ' }, + { code: 'NU', name: 'Niue', dialCode: '+683', flag: '๐Ÿ‡ณ๐Ÿ‡บ' }, + { code: 'NF', name: 'Norfolk Island', dialCode: '+672', flag: '๐Ÿ‡ณ๐Ÿ‡ซ' }, + { code: 'MP', name: 'Northern Mariana Islands', dialCode: '+1670', flag: '๐Ÿ‡ฒ๐Ÿ‡ต' }, + { code: 'NO', name: 'Norway', dialCode: '+47', flag: '๐Ÿ‡ณ๐Ÿ‡ด' }, + { code: 'OM', name: 'Oman', dialCode: '+968', flag: '๐Ÿ‡ด๐Ÿ‡ฒ' }, + { code: 'PK', name: 'Pakistan', dialCode: '+92', flag: '๐Ÿ‡ต๐Ÿ‡ฐ' }, + { code: 'PW', name: 'Palau', dialCode: '+680', flag: '๐Ÿ‡ต๐Ÿ‡ผ' }, + { code: 'PS', name: 'Palestine', dialCode: '+970', flag: '๐Ÿ‡ต๐Ÿ‡ธ' }, + { code: 'PA', name: 'Panama', dialCode: '+507', flag: '๐Ÿ‡ต๐Ÿ‡ฆ' }, + { code: 'PG', name: 'Papua New Guinea', dialCode: '+675', flag: '๐Ÿ‡ต๐Ÿ‡ฌ' }, + { code: 'PY', name: 'Paraguay', dialCode: '+595', flag: '๐Ÿ‡ต๐Ÿ‡พ' }, + { code: 'PE', name: 'Peru', dialCode: '+51', flag: '๐Ÿ‡ต๐Ÿ‡ช' }, + { code: 'PH', name: 'Philippines', dialCode: '+63', flag: '๐Ÿ‡ต๐Ÿ‡ญ' }, + { code: 'PN', name: 'Pitcairn Islands', dialCode: '+64', flag: '๐Ÿ‡ต๐Ÿ‡ณ' }, + { code: 'PL', name: 'Poland', dialCode: '+48', flag: '๐Ÿ‡ต๐Ÿ‡ฑ' }, + { code: 'PT', name: 'Portugal', dialCode: '+351', flag: '๐Ÿ‡ต๐Ÿ‡น' }, + { code: 'PR', name: 'Puerto Rico', dialCode: '+1787', flag: '๐Ÿ‡ต๐Ÿ‡ท' }, + { code: 'QA', name: 'Qatar', dialCode: '+974', flag: '๐Ÿ‡ถ๐Ÿ‡ฆ' }, + { code: 'RE', name: 'Rรฉunion', dialCode: '+262', flag: '๐Ÿ‡ท๐Ÿ‡ช' }, + { code: 'RO', name: 'Romania', dialCode: '+40', flag: '๐Ÿ‡ท๐Ÿ‡ด' }, + { code: 'RU', name: 'Russia', dialCode: '+7', flag: '๐Ÿ‡ท๐Ÿ‡บ' }, + { code: 'RW', name: 'Rwanda', dialCode: '+250', flag: '๐Ÿ‡ท๐Ÿ‡ผ' }, + { code: 'BL', name: 'Saint Barthรฉlemy', dialCode: '+590', flag: '๐Ÿ‡ง๐Ÿ‡ฑ' }, + { code: 'SH', name: 'Saint Helena', dialCode: '+290', flag: '๐Ÿ‡ธ๐Ÿ‡ญ' }, + { code: 'KN', name: 'Saint Kitts and Nevis', dialCode: '+1869', flag: '๐Ÿ‡ฐ๐Ÿ‡ณ' }, + { code: 'LC', name: 'Saint Lucia', dialCode: '+1758', flag: '๐Ÿ‡ฑ๐Ÿ‡จ' }, + { code: 'MF', name: 'Saint Martin', dialCode: '+590', flag: '๐Ÿ‡ฒ๐Ÿ‡ซ' }, + { code: 'PM', name: 'Saint Pierre and Miquelon', dialCode: '+508', flag: '๐Ÿ‡ต๐Ÿ‡ฒ' }, + { code: 'VC', name: 'Saint Vincent and the Grenadines', dialCode: '+1784', flag: '๐Ÿ‡ป๐Ÿ‡จ' }, + { code: 'WS', name: 'Samoa', dialCode: '+685', flag: '๐Ÿ‡ผ๐Ÿ‡ธ' }, + { code: 'SM', name: 'San Marino', dialCode: '+378', flag: '๐Ÿ‡ธ๐Ÿ‡ฒ' }, + { code: 'ST', name: 'Sรฃo Tomรฉ and Prรญncipe', dialCode: '+239', flag: '๐Ÿ‡ธ๐Ÿ‡น' }, + { code: 'SA', name: 'Saudi Arabia', dialCode: '+966', flag: '๐Ÿ‡ธ๐Ÿ‡ฆ' }, + { code: 'SN', name: 'Senegal', dialCode: '+221', flag: '๐Ÿ‡ธ๐Ÿ‡ณ' }, + { code: 'RS', name: 'Serbia', dialCode: '+381', flag: '๐Ÿ‡ท๐Ÿ‡ธ' }, + { code: 'SC', name: 'Seychelles', dialCode: '+248', flag: '๐Ÿ‡ธ๐Ÿ‡จ' }, + { code: 'SL', name: 'Sierra Leone', dialCode: '+232', flag: '๐Ÿ‡ธ๐Ÿ‡ฑ' }, + { code: 'SG', name: 'Singapore', dialCode: '+65', flag: '๐Ÿ‡ธ๐Ÿ‡ฌ' }, + { code: 'SX', name: 'Sint Maarten', dialCode: '+1721', flag: '๐Ÿ‡ธ๐Ÿ‡ฝ' }, + { code: 'SK', name: 'Slovakia', dialCode: '+421', flag: '๐Ÿ‡ธ๐Ÿ‡ฐ' }, + { code: 'SI', name: 'Slovenia', dialCode: '+386', flag: '๐Ÿ‡ธ๐Ÿ‡ฎ' }, + { code: 'SB', name: 'Solomon Islands', dialCode: '+677', flag: '๐Ÿ‡ธ๐Ÿ‡ง' }, + { code: 'SO', name: 'Somalia', dialCode: '+252', flag: '๐Ÿ‡ธ๐Ÿ‡ด' }, + { code: 'ZA', name: 'South Africa', dialCode: '+27', flag: '๐Ÿ‡ฟ๐Ÿ‡ฆ' }, + { code: 'GS', name: 'South Georgia and the South Sandwich Islands', dialCode: '+500', flag: '๐Ÿ‡ฌ๐Ÿ‡ธ' }, + { code: 'SS', name: 'South Sudan', dialCode: '+211', flag: '๐Ÿ‡ธ๐Ÿ‡ธ' }, + { code: 'ES', name: 'Spain', dialCode: '+34', flag: '๐Ÿ‡ช๐Ÿ‡ธ' }, + { code: 'LK', name: 'Sri Lanka', dialCode: '+94', flag: '๐Ÿ‡ฑ๐Ÿ‡ฐ' }, + { code: 'SD', name: 'Sudan', dialCode: '+249', flag: '๐Ÿ‡ธ๐Ÿ‡ฉ' }, + { code: 'SR', name: 'Suriname', dialCode: '+597', flag: '๐Ÿ‡ธ๐Ÿ‡ท' }, + { code: 'SJ', name: 'Svalbard and Jan Mayen', dialCode: '+47', flag: '๐Ÿ‡ธ๐Ÿ‡ฏ' }, + { code: 'SZ', name: 'Eswatini', dialCode: '+268', flag: '๐Ÿ‡ธ๐Ÿ‡ฟ' }, + { code: 'SE', name: 'Sweden', dialCode: '+46', flag: '๐Ÿ‡ธ๐Ÿ‡ช' }, + { code: 'CH', name: 'Switzerland', dialCode: '+41', flag: '๐Ÿ‡จ๐Ÿ‡ญ' }, + { code: 'SY', name: 'Syria', dialCode: '+963', flag: '๐Ÿ‡ธ๐Ÿ‡พ' }, + { code: 'TW', name: 'Taiwan', dialCode: '+886', flag: '๐Ÿ‡น๐Ÿ‡ผ' }, + { code: 'TJ', name: 'Tajikistan', dialCode: '+992', flag: '๐Ÿ‡น๐Ÿ‡ฏ' }, + { code: 'TZ', name: 'Tanzania', dialCode: '+255', flag: '๐Ÿ‡น๐Ÿ‡ฟ' }, + { code: 'TH', name: 'Thailand', dialCode: '+66', flag: '๐Ÿ‡น๐Ÿ‡ญ' }, + { code: 'TL', name: 'Timor-Leste', dialCode: '+670', flag: '๐Ÿ‡น๐Ÿ‡ฑ' }, + { code: 'TG', name: 'Togo', dialCode: '+228', flag: '๐Ÿ‡น๐Ÿ‡ฌ' }, + { code: 'TK', name: 'Tokelau', dialCode: '+690', flag: '๐Ÿ‡น๐Ÿ‡ฐ' }, + { code: 'TO', name: 'Tonga', dialCode: '+676', flag: '๐Ÿ‡น๐Ÿ‡ด' }, + { code: 'TT', name: 'Trinidad and Tobago', dialCode: '+1868', flag: '๐Ÿ‡น๐Ÿ‡น' }, + { code: 'TN', name: 'Tunisia', dialCode: '+216', flag: '๐Ÿ‡น๐Ÿ‡ณ' }, + { code: 'TR', name: 'Turkey', dialCode: '+90', flag: '๐Ÿ‡น๐Ÿ‡ท' }, + { code: 'TM', name: 'Turkmenistan', dialCode: '+993', flag: '๐Ÿ‡น๐Ÿ‡ฒ' }, + { code: 'TC', name: 'Turks and Caicos Islands', dialCode: '+1649', flag: '๐Ÿ‡น๐Ÿ‡จ' }, + { code: 'TV', name: 'Tuvalu', dialCode: '+688', flag: '๐Ÿ‡น๐Ÿ‡ป' }, + { code: 'UG', name: 'Uganda', dialCode: '+256', flag: '๐Ÿ‡บ๐Ÿ‡ฌ' }, + { code: 'UA', name: 'Ukraine', dialCode: '+380', flag: '๐Ÿ‡บ๐Ÿ‡ฆ' }, + { code: 'AE', name: 'United Arab Emirates', dialCode: '+971', flag: '๐Ÿ‡ฆ๐Ÿ‡ช' }, + { code: 'GB', name: 'United Kingdom', dialCode: '+44', flag: '๐Ÿ‡ฌ๐Ÿ‡ง' }, + { code: 'US', name: 'United States', dialCode: '+1', flag: '๐Ÿ‡บ๐Ÿ‡ธ' }, + { code: 'UM', name: 'United States Minor Outlying Islands', dialCode: '+1', flag: '๐Ÿ‡บ๐Ÿ‡ฒ' }, + { code: 'UY', name: 'Uruguay', dialCode: '+598', flag: '๐Ÿ‡บ๐Ÿ‡พ' }, + { code: 'UZ', name: 'Uzbekistan', dialCode: '+998', flag: '๐Ÿ‡บ๐Ÿ‡ฟ' }, + { code: 'VU', name: 'Vanuatu', dialCode: '+678', flag: '๐Ÿ‡ป๐Ÿ‡บ' }, + { code: 'VE', name: 'Venezuela', dialCode: '+58', flag: '๐Ÿ‡ป๐Ÿ‡ช' }, + { code: 'VN', name: 'Vietnam', dialCode: '+84', flag: '๐Ÿ‡ป๐Ÿ‡ณ' }, + { code: 'VG', name: 'British Virgin Islands', dialCode: '+1284', flag: '๐Ÿ‡ป๐Ÿ‡ฌ' }, + { code: 'VI', name: 'U.S. Virgin Islands', dialCode: '+1340', flag: '๐Ÿ‡ป๐Ÿ‡ฎ' }, + { code: 'WF', name: 'Wallis and Futuna', dialCode: '+681', flag: '๐Ÿ‡ผ๐Ÿ‡ซ' }, + { code: 'EH', name: 'Western Sahara', dialCode: '+212', flag: '๐Ÿ‡ช๐Ÿ‡ญ' }, + { code: 'YE', name: 'Yemen', dialCode: '+967', flag: '๐Ÿ‡พ๐Ÿ‡ช' }, + { code: 'ZM', name: 'Zambia', dialCode: '+260', flag: '๐Ÿ‡ฟ๐Ÿ‡ฒ' }, + { code: 'ZW', name: 'Zimbabwe', dialCode: '+263', flag: '๐Ÿ‡ฟ๐Ÿ‡ผ' } + ]; + ngOnInit(): void { super.ngOnInit(); this.configureFromWidgetParams(); - } - - ngAfterViewInit(): void { - // Any additional initialization if needed + this.initializePhoneNumber(); } private configureFromWidgetParams(): void { @@ -54,8 +309,142 @@ export class PhoneRowComponent extends BaseRowFieldComponent implements OnInit, } } - onPhoneNumberChange(phoneNumber: string): void { - this.value = phoneNumber; + private initializePhoneNumber(): void { + if (this.value) { + this.parseExistingPhoneNumber(this.value); + } else { + // Set default country + this.selectedCountry = this.countries.find(c => c.code === this.preferredCountries[0]) || this.countries[0]; + } + } + + private parseExistingPhoneNumber(fullNumber: string): void { + // Clean the number first (remove spaces, dashes, parentheses) + const cleanNumber = fullNumber.replace(/[\s\-\(\)]/g, ''); + + // Try to extract country code from existing number + // Sort countries by dial code length (longest first) to match correctly + const sortedByDialCode = this.countries.sort((a, b) => b.dialCode.length - a.dialCode.length); + + const country = sortedByDialCode.find(c => { + const cleanDialCode = c.dialCode.replace('+', ''); + return cleanNumber.startsWith('+' + cleanDialCode) || cleanNumber.startsWith(cleanDialCode); + }); + + if (country) { + this.selectedCountry = country; + const cleanDialCode = country.dialCode.replace('+', ''); + let remainingNumber = cleanNumber; + + // Remove the country code from the beginning + if (remainingNumber.startsWith('+' + cleanDialCode)) { + remainingNumber = remainingNumber.substring(cleanDialCode.length + 1); + } else if (remainingNumber.startsWith(cleanDialCode)) { + remainingNumber = remainingNumber.substring(cleanDialCode.length); + } + + this.phoneNumber = remainingNumber; + } else { + // Default to first preferred country + this.selectedCountry = this.countries.find(c => c.code === this.preferredCountries[0]) || this.countries[0]; + this.phoneNumber = fullNumber; + } + } + + onCountryChange(): void { + this.updateFullPhoneNumber(); + } + + onPhoneNumberChange(): void { + // Check if user entered a full international number (starts with +) + if (this.phoneNumber.startsWith('+')) { + this.detectCountryFromInput(); + } else { + this.updateFullPhoneNumber(); + } + } + + private detectCountryFromInput(): void { + if (!this.phoneNumber.startsWith('+')) { + return; + } + + // Clean the number (remove spaces, dashes, parentheses) + const cleanNumber = this.phoneNumber.replace(/[\s\-\(\)]/g, ''); + + // Sort countries by dial code length (longest first) to match correctly + const sortedByDialCode = [...this.countries].sort((a, b) => b.dialCode.length - a.dialCode.length); + + const detectedCountry = sortedByDialCode.find(c => { + return cleanNumber.startsWith(c.dialCode); + }); + + if (detectedCountry) { + // Update selected country + this.selectedCountry = detectedCountry; + + // Extract the remaining phone number (without country code) + const remainingNumber = cleanNumber.substring(detectedCountry.dialCode.length); + + // Update the phone number field to show only the local part + this.phoneNumber = remainingNumber; + + // Update the full value + this.updateFullPhoneNumber(); + } else { + // If no country detected, treat as full number + this.updateFullPhoneNumber(); + } + } + + private updateFullPhoneNumber(): void { + if (this.phoneNumber.startsWith('+')) { + // If user entered a full international number, use it as-is + this.value = this.phoneNumber; + } else if (this.selectedCountry && this.phoneNumber) { + // Combine country code with local number + this.value = `${this.selectedCountry.dialCode} ${this.phoneNumber}`; + } else if (this.phoneNumber) { + // Just the number without country code + this.value = this.phoneNumber; + } else { + this.value = ''; + } this.onFieldChange.emit(this.value); } + + get sortedCountries(): CountryCode[] { + const preferred = this.countries.filter(c => this.preferredCountries.includes(c.code)); + const others = this.countries.filter(c => !this.preferredCountries.includes(c.code)); + return [...preferred, ...others]; + } + + get placeholder(): string { + if (!this.enablePlaceholder) return ''; + return this.selectedCountry ? `Phone number for ${this.selectedCountry.name}` : 'Phone number'; + } + + getPhoneNumberPlaceholder(): string { + if (!this.enablePlaceholder) return ''; + if (this.selectedCountry) { + return `Local number or ${this.selectedCountry.dialCode}1234567890`; + } + return 'Enter +1234567890 or select country'; + } + + isValidPhoneNumber(): boolean { + if (!this.phoneValidation) return true; + if (!this.phoneNumber) return true; // Empty is valid (let required validation handle it) + + // If it's an international number (starts with +), validate differently + if (this.phoneNumber.startsWith('+')) { + const phoneRegex = /^\+[\d\s\-\(\)]+$/; + const cleanNumber = this.phoneNumber.replace(/\D/g, ''); + return phoneRegex.test(this.phoneNumber) && cleanNumber.length >= 8; // At least country code + 7 digits + } else { + // Local number validation - digits, spaces, dashes, parentheses + const phoneRegex = /^[\d\s\-\(\)]+$/; + return phoneRegex.test(this.phoneNumber) && this.phoneNumber.replace(/\D/g, '').length >= 7; + } + } } \ No newline at end of file diff --git a/frontend/yarn.lock b/frontend/yarn.lock index 4a68616e6..aa57a9d1e 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -6253,12 +6253,10 @@ __metadata: karma-coverage-istanbul-reporter: ^3.0.3 karma-jasmine: ~5.1.0 karma-jasmine-html-reporter: ^2.1.0 - libphonenumber-js: ^1.12.9 lodash: ^4.17.21 ng-dynamic-component: ^10.7.0 ngx-cookie-service: ^19.0.0 ngx-markdown: ^19.0.0 - ngx-mat-input-tel: ^19.0.0 ngx-stripe: ^19.0.0 pluralize: ^8.0.0 postgres-interval: ^4.0.2 @@ -8428,13 +8426,6 @@ __metadata: languageName: node linkType: hard -"libphonenumber-js@npm:^1.12.9": - version: 1.12.9 - resolution: "libphonenumber-js@npm:1.12.9" - checksum: 9d01151ffa1d0f634ebbc4e7d5cde6baa7c53765e162745cb2dd85815e04b42847ff2e4c54fe172121c335c86cd6f5ce9192af62237099213e799de653d5a6dd - languageName: node - linkType: hard - "license-webpack-plugin@npm:4.0.2": version: 4.0.2 resolution: "license-webpack-plugin@npm:4.0.2" @@ -9264,22 +9255,6 @@ __metadata: languageName: node linkType: hard -"ngx-mat-input-tel@npm:^19.0.0": - version: 19.5.1 - resolution: "ngx-mat-input-tel@npm:19.5.1" - dependencies: - tslib: ^2.x - peerDependencies: - "@angular/common": ">=19.x" - "@angular/core": ">=19.x" - "@angular/forms": ">=19.x" - "@angular/platform-browser": ">=19.x" - "@angular/platform-browser-dynamic": ">=19.x" - libphonenumber-js: ^1.12.5 - checksum: 1cd9d15f5cd7cbec0e528707b9dfc7e2bfc8a2ad32fa1ad551559b9367f283e2217d188bade72f213d13f9efc90ee45316d9f167193f137850b7a5e6890cdf27 - languageName: node - linkType: hard - "ngx-stripe@npm:^19.0.0": version: 19.0.0 resolution: "ngx-stripe@npm:19.0.0" @@ -11882,7 +11857,7 @@ __metadata: languageName: node linkType: hard -"tslib@npm:2.8.1, tslib@npm:^2.0.0, tslib@npm:^2.0.1, tslib@npm:^2.1.0, tslib@npm:^2.3.0, tslib@npm:^2.4.1, tslib@npm:^2.5.0, tslib@npm:^2.7.0, tslib@npm:^2.8.0, tslib@npm:^2.8.1, tslib@npm:^2.x": +"tslib@npm:2.8.1, tslib@npm:^2.0.0, tslib@npm:^2.0.1, tslib@npm:^2.1.0, tslib@npm:^2.3.0, tslib@npm:^2.4.1, tslib@npm:^2.5.0, tslib@npm:^2.7.0, tslib@npm:^2.8.0, tslib@npm:^2.8.1": version: 2.8.1 resolution: "tslib@npm:2.8.1" checksum: e4aba30e632b8c8902b47587fd13345e2827fa639e7c3121074d5ee0880723282411a8838f830b55100cbe4517672f84a2472667d355b81e8af165a55dc6203a From 26e462061731110ae383979697c5a1f0ffbf56c2 Mon Sep 17 00:00:00 2001 From: Andrii Kostenko Date: Wed, 2 Jul 2025 09:11:28 +0000 Subject: [PATCH 07/10] libphonenumber-js --- .../row-fields/phone/phone.component.html | 8 +- .../row-fields/phone/phone.component.ts | 215 ++++++++++++------ 2 files changed, 146 insertions(+), 77 deletions(-) diff --git a/frontend/src/app/components/ui-components/row-fields/phone/phone.component.html b/frontend/src/app/components/ui-components/row-fields/phone/phone.component.html index 56b31e28e..c193fa86f 100644 --- a/frontend/src/app/components/ui-components/row-fields/phone/phone.component.html +++ b/frontend/src/app/components/ui-components/row-fields/phone/phone.component.html @@ -27,16 +27,16 @@ - {{selectedCountry.dialCode}} - Enter international number (e.g., +1234567890) or select country first - + {{selectedCountry.dialCode}} + Enter international number (e.g., +1234567890) or select country first + Invalid phone number format
diff --git a/frontend/src/app/components/ui-components/row-fields/phone/phone.component.ts b/frontend/src/app/components/ui-components/row-fields/phone/phone.component.ts index 9dd8884b3..1bee507c5 100644 --- a/frontend/src/app/components/ui-components/row-fields/phone/phone.component.ts +++ b/frontend/src/app/components/ui-components/row-fields/phone/phone.component.ts @@ -5,6 +5,7 @@ import { MatFormFieldModule } from '@angular/material/form-field'; import { MatInputModule } from '@angular/material/input'; import { MatSelectModule } from '@angular/material/select'; import { CommonModule } from '@angular/common'; +import { parsePhoneNumber, getCountries, getCountryCallingCode, AsYouType, CountryCode as LibPhoneCountryCode } from 'libphonenumber-js'; interface CountryCode { code: string; @@ -33,6 +34,8 @@ export class PhoneRowComponent extends BaseRowFieldComponent implements OnInit { selectedCountry: CountryCode; phoneNumber: string = ''; + displayPhoneNumber: string = ''; + private formatter: AsYouType | null = null; countries: CountryCode[] = [ { code: 'AF', name: 'Afghanistan', dialCode: '+93', flag: '๐Ÿ‡ฆ๐Ÿ‡ซ' }, @@ -314,102 +317,163 @@ export class PhoneRowComponent extends BaseRowFieldComponent implements OnInit { this.parseExistingPhoneNumber(this.value); } else { // Set default country - this.selectedCountry = this.countries.find(c => c.code === this.preferredCountries[0]) || this.countries[0]; + this.setDefaultCountry(); + this.displayPhoneNumber = ''; } } private parseExistingPhoneNumber(fullNumber: string): void { - // Clean the number first (remove spaces, dashes, parentheses) - const cleanNumber = fullNumber.replace(/[\s\-\(\)]/g, ''); + let phoneNumber; - // Try to extract country code from existing number - // Sort countries by dial code length (longest first) to match correctly - const sortedByDialCode = this.countries.sort((a, b) => b.dialCode.length - a.dialCode.length); + try { + // First try to parse as international number + phoneNumber = parsePhoneNumber(fullNumber); + } catch (error) { + // Will try with default country below + } - const country = sortedByDialCode.find(c => { - const cleanDialCode = c.dialCode.replace('+', ''); - return cleanNumber.startsWith('+' + cleanDialCode) || cleanNumber.startsWith(cleanDialCode); - }); + // If that failed or didn't detect country, try with default country + if (!phoneNumber || !phoneNumber.country) { + try { + const defaultCountryCode = this.preferredCountries[0] || 'US'; + phoneNumber = parsePhoneNumber(fullNumber, defaultCountryCode as LibPhoneCountryCode); + } catch (error) { + console.warn('Failed to parse with default country as well:', error); + } + } - if (country) { - this.selectedCountry = country; - const cleanDialCode = country.dialCode.replace('+', ''); - let remainingNumber = cleanNumber; - - // Remove the country code from the beginning - if (remainingNumber.startsWith('+' + cleanDialCode)) { - remainingNumber = remainingNumber.substring(cleanDialCode.length + 1); - } else if (remainingNumber.startsWith(cleanDialCode)) { - remainingNumber = remainingNumber.substring(cleanDialCode.length); + if (phoneNumber && phoneNumber.country) { + // Find the country in our list - exact match by country code + const country = this.countries.find(c => c.code === phoneNumber.country); + if (country) { + this.selectedCountry = country; + this.phoneNumber = phoneNumber.nationalNumber; + this.displayPhoneNumber = phoneNumber.formatNational(); + this.initializeFormatter(); + return; + } else { + console.warn('Country not found in list:', phoneNumber.country); } - - this.phoneNumber = remainingNumber; + } + + // Fallback: use default country and original number + this.setDefaultCountry(); + this.phoneNumber = fullNumber.replace(/\D/g, ''); + + // Try to format with default country formatter + if (this.formatter && this.phoneNumber) { + this.formatter.reset(); + this.displayPhoneNumber = this.formatter.input(this.phoneNumber); } else { - // Default to first preferred country - this.selectedCountry = this.countries.find(c => c.code === this.preferredCountries[0]) || this.countries[0]; - this.phoneNumber = fullNumber; + this.displayPhoneNumber = fullNumber; + } + } + + private setDefaultCountry(): void { + this.selectedCountry = this.countries.find(c => c.code === this.preferredCountries[0]) || this.countries[0]; + this.initializeFormatter(); + } + + private initializeFormatter(): void { + if (this.selectedCountry) { + this.formatter = new AsYouType(this.selectedCountry.code as LibPhoneCountryCode); } } onCountryChange(): void { - this.updateFullPhoneNumber(); + this.initializeFormatter(); + this.formatAndUpdatePhoneNumber(); } onPhoneNumberChange(): void { // Check if user entered a full international number (starts with +) - if (this.phoneNumber.startsWith('+')) { + if (this.displayPhoneNumber.startsWith('+')) { this.detectCountryFromInput(); } else { - this.updateFullPhoneNumber(); + this.formatAndUpdatePhoneNumber(); } } - private detectCountryFromInput(): void { - if (!this.phoneNumber.startsWith('+')) { + private formatAndUpdatePhoneNumber(): void { + if (!this.displayPhoneNumber) { + this.phoneNumber = ''; + this.value = ''; + this.onFieldChange.emit(this.value); return; } - // Clean the number (remove spaces, dashes, parentheses) - const cleanNumber = this.phoneNumber.replace(/[\s\-\(\)]/g, ''); - - // Sort countries by dial code length (longest first) to match correctly - const sortedByDialCode = [...this.countries].sort((a, b) => b.dialCode.length - a.dialCode.length); - - const detectedCountry = sortedByDialCode.find(c => { - return cleanNumber.startsWith(c.dialCode); - }); - - if (detectedCountry) { - // Update selected country - this.selectedCountry = detectedCountry; - - // Extract the remaining phone number (without country code) - const remainingNumber = cleanNumber.substring(detectedCountry.dialCode.length); + if (this.formatter && !this.displayPhoneNumber.startsWith('+')) { + this.formatter.reset(); + const formatted = this.formatter.input(this.displayPhoneNumber); + this.displayPhoneNumber = formatted; - // Update the phone number field to show only the local part - this.phoneNumber = remainingNumber; - - // Update the full value - this.updateFullPhoneNumber(); + // Extract raw number for storage + this.phoneNumber = this.displayPhoneNumber.replace(/\D/g, ''); } else { - // If no country detected, treat as full number - this.updateFullPhoneNumber(); + this.phoneNumber = this.displayPhoneNumber.replace(/\D/g, ''); } + + this.updateFullPhoneNumber(); + } + + private detectCountryFromInput(): void { + if (!this.displayPhoneNumber.startsWith('+')) { + return; + } + + try { + const phoneNumber = parsePhoneNumber(this.displayPhoneNumber); + if (phoneNumber && phoneNumber.country) { + const detectedCountry = this.countries.find(c => c.code === phoneNumber.country); + if (detectedCountry) { + this.selectedCountry = detectedCountry; + this.phoneNumber = phoneNumber.nationalNumber; + this.displayPhoneNumber = phoneNumber.formatNational(); + this.initializeFormatter(); + this.updateFullPhoneNumber(); + return; + } + } + } catch (error) { + console.warn('Could not detect country from input:', this.displayPhoneNumber, error); + } + + // If detection failed, update with current input + this.phoneNumber = this.displayPhoneNumber.replace(/\D/g, ''); + this.updateFullPhoneNumber(); } private updateFullPhoneNumber(): void { - if (this.phoneNumber.startsWith('+')) { - // If user entered a full international number, use it as-is - this.value = this.phoneNumber; - } else if (this.selectedCountry && this.phoneNumber) { - // Combine country code with local number - this.value = `${this.selectedCountry.dialCode} ${this.phoneNumber}`; - } else if (this.phoneNumber) { - // Just the number without country code - this.value = this.phoneNumber; - } else { + if (!this.displayPhoneNumber && !this.phoneNumber) { this.value = ''; + this.onFieldChange.emit(this.value); + return; } + + try { + let phoneNumber; + + if (this.displayPhoneNumber.startsWith('+')) { + // User entered full international number + phoneNumber = parsePhoneNumber(this.displayPhoneNumber); + } else if (this.selectedCountry && this.displayPhoneNumber) { + // User entered local number, parse with selected country + phoneNumber = parsePhoneNumber(this.displayPhoneNumber, this.selectedCountry.code as LibPhoneCountryCode); + } + + if (phoneNumber && phoneNumber.isValid()) { + // Store in international format without spaces (E164) + this.value = phoneNumber.number; // E164 format: +380671111111 + } else { + // Fallback: clean the display number + this.value = this.displayPhoneNumber.replace(/\s/g, ''); + } + } catch (error) { + console.warn('Error formatting phone number:', error); + // Fallback: clean the display number + this.value = this.displayPhoneNumber.replace(/\s/g, ''); + } + this.onFieldChange.emit(this.value); } @@ -434,17 +498,22 @@ export class PhoneRowComponent extends BaseRowFieldComponent implements OnInit { isValidPhoneNumber(): boolean { if (!this.phoneValidation) return true; - if (!this.phoneNumber) return true; // Empty is valid (let required validation handle it) + if (!this.displayPhoneNumber) return true; // Empty is valid (let required validation handle it) - // If it's an international number (starts with +), validate differently - if (this.phoneNumber.startsWith('+')) { - const phoneRegex = /^\+[\d\s\-\(\)]+$/; - const cleanNumber = this.phoneNumber.replace(/\D/g, ''); - return phoneRegex.test(this.phoneNumber) && cleanNumber.length >= 8; // At least country code + 7 digits - } else { - // Local number validation - digits, spaces, dashes, parentheses - const phoneRegex = /^[\d\s\-\(\)]+$/; - return phoneRegex.test(this.phoneNumber) && this.phoneNumber.replace(/\D/g, '').length >= 7; + try { + let phoneNumber; + + if (this.displayPhoneNumber.startsWith('+')) { + phoneNumber = parsePhoneNumber(this.displayPhoneNumber); + } else if (this.selectedCountry) { + phoneNumber = parsePhoneNumber(this.displayPhoneNumber, this.selectedCountry.code as LibPhoneCountryCode); + } else { + return false; + } + + return phoneNumber ? phoneNumber.isValid() : false; + } catch (error) { + return false; } } } \ No newline at end of file From 50ead42c767e8dcdd6df45192bd8b53d57c591f4 Mon Sep 17 00:00:00 2001 From: Andrii Kostenko Date: Wed, 2 Jul 2025 09:12:22 +0000 Subject: [PATCH 08/10] libphonenumber-js --- frontend/package.json | 1 + frontend/yarn.lock | 8 ++++++++ package.json | 5 +---- 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/frontend/package.json b/frontend/package.json index 5afd1cd56..0c6d80686 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -41,6 +41,7 @@ "date-fns": "^4.1.0", "ipaddr.js": "^2.2.0", "json5": "^2.2.3", + "libphonenumber-js": "^1.12.9", "lodash": "^4.17.21", "ng-dynamic-component": "^10.7.0", "ngx-cookie-service": "^19.0.0", diff --git a/frontend/yarn.lock b/frontend/yarn.lock index aa57a9d1e..8e1440c0f 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -6253,6 +6253,7 @@ __metadata: karma-coverage-istanbul-reporter: ^3.0.3 karma-jasmine: ~5.1.0 karma-jasmine-html-reporter: ^2.1.0 + libphonenumber-js: ^1.12.9 lodash: ^4.17.21 ng-dynamic-component: ^10.7.0 ngx-cookie-service: ^19.0.0 @@ -8426,6 +8427,13 @@ __metadata: languageName: node linkType: hard +"libphonenumber-js@npm:^1.12.9": + version: 1.12.9 + resolution: "libphonenumber-js@npm:1.12.9" + checksum: 9d01151ffa1d0f634ebbc4e7d5cde6baa7c53765e162745cb2dd85815e04b42847ff2e4c54fe172121c335c86cd6f5ce9192af62237099213e799de653d5a6dd + languageName: node + linkType: hard + "license-webpack-plugin@npm:4.0.2": version: 4.0.2 resolution: "license-webpack-plugin@npm:4.0.2" diff --git a/package.json b/package.json index ba6fcf0fa..0189c8da2 100644 --- a/package.json +++ b/package.json @@ -6,8 +6,5 @@ "rocketadmin-agent", "shared-code" ], - "packageManager": "yarn@3.4.1", - "dependencies": { - "libphonenumber-js": "^1.12.9" - } + "packageManager": "yarn@3.4.1" } From 12b365ed6c943d7e39e5e92d3de1b254732fe2eb Mon Sep 17 00:00:00 2001 From: Andrii Kostenko Date: Wed, 2 Jul 2025 11:09:32 +0000 Subject: [PATCH 09/10] remove unused settings --- .../dashboard/db-table-widgets/db-table-widgets.component.ts | 4 +--- .../ui-components/row-fields/phone/phone.component.ts | 4 ---- 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/frontend/src/app/components/dashboard/db-table-widgets/db-table-widgets.component.ts b/frontend/src/app/components/dashboard/db-table-widgets/db-table-widgets.component.ts index f20de3d84..4e63aef7e 100644 --- a/frontend/src/app/components/dashboard/db-table-widgets/db-table-widgets.component.ts +++ b/frontend/src/app/components/dashboard/db-table-widgets/db-table-widgets.component.ts @@ -168,9 +168,7 @@ export class DbTableWidgetsComponent implements OnInit { { "preferred_countries": ["US", "GB", "CA"], "enable_placeholder": true, - "enable_auto_country_select": true, - "phone_validation": true, - "format": "international" + "phone_validation": true } `, Foreign_key: `// Provide settings for foreign key widget diff --git a/frontend/src/app/components/ui-components/row-fields/phone/phone.component.ts b/frontend/src/app/components/ui-components/row-fields/phone/phone.component.ts index 1bee507c5..2b38dc0f9 100644 --- a/frontend/src/app/components/ui-components/row-fields/phone/phone.component.ts +++ b/frontend/src/app/components/ui-components/row-fields/phone/phone.component.ts @@ -29,7 +29,6 @@ export class PhoneRowComponent extends BaseRowFieldComponent implements OnInit { preferredCountries: string[] = ['US', 'GB']; enablePlaceholder: boolean = true; - enableAutoCountrySelect: boolean = true; phoneValidation: boolean = true; selectedCountry: CountryCode; @@ -302,9 +301,6 @@ export class PhoneRowComponent extends BaseRowFieldComponent implements OnInit { this.enablePlaceholder = params.enable_placeholder; } - if (typeof params.enable_auto_country_select === 'boolean') { - this.enableAutoCountrySelect = params.enable_auto_country_select; - } if (typeof params.phone_validation === 'boolean') { this.phoneValidation = params.phone_validation; From 56e831b7da0569a7defe16993be0cc2e4d4d4215 Mon Sep 17 00:00:00 2001 From: Andrii Kostenko Date: Wed, 2 Jul 2025 12:12:23 +0000 Subject: [PATCH 10/10] country autocomplete, examples --- CLAUDE.md | 8 + .../row-fields/phone/phone.component.html | 30 +-- .../row-fields/phone/phone.component.ts | 201 +++++++++++++++++- 3 files changed, 221 insertions(+), 18 deletions(-) create mode 100644 CLAUDE.md diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 000000000..23b8ed94f --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,8 @@ +# Claude Code Configuration + +## Frontend Tests + +To run frontend tests: +```bash +cd frontend && yarn test --browsers=ChromeHeadlessCustom --no-watch --no-progress +``` \ No newline at end of file diff --git a/frontend/src/app/components/ui-components/row-fields/phone/phone.component.html b/frontend/src/app/components/ui-components/row-fields/phone/phone.component.html index c193fa86f..0dcbdcb6d 100644 --- a/frontend/src/app/components/ui-components/row-fields/phone/phone.component.html +++ b/frontend/src/app/components/ui-components/row-fields/phone/phone.component.html @@ -1,25 +1,27 @@
Country - - - - {{selectedCountry.flag}} - {{selectedCountry.dialCode}} - {{selectedCountry.name}} - - - + + + + {{country.flag}} {{country.dialCode}} {{country.name}} - + @@ -34,7 +36,7 @@ [disabled]="disabled" [class.invalid]="phoneValidation && !isValidPhoneNumber()" attr.data-testid="record-{{label}}-phone"> - {{selectedCountry.dialCode}} + Example: {{getExamplePhoneNumber()}} Enter international number (e.g., +1234567890) or select country first Invalid phone number format diff --git a/frontend/src/app/components/ui-components/row-fields/phone/phone.component.ts b/frontend/src/app/components/ui-components/row-fields/phone/phone.component.ts index 2b38dc0f9..47732e4f9 100644 --- a/frontend/src/app/components/ui-components/row-fields/phone/phone.component.ts +++ b/frontend/src/app/components/ui-components/row-fields/phone/phone.component.ts @@ -4,7 +4,11 @@ import { FormsModule } from '@angular/forms'; import { MatFormFieldModule } from '@angular/material/form-field'; import { MatInputModule } from '@angular/material/input'; import { MatSelectModule } from '@angular/material/select'; +import { MatAutocompleteModule } from '@angular/material/autocomplete'; import { CommonModule } from '@angular/common'; +import { Observable, map, startWith } from 'rxjs'; +import { FormControl } from '@angular/forms'; +import { ReactiveFormsModule } from '@angular/forms'; import { parsePhoneNumber, getCountries, getCountryCallingCode, AsYouType, CountryCode as LibPhoneCountryCode } from 'libphonenumber-js'; interface CountryCode { @@ -20,7 +24,7 @@ interface CountryCode { selector: 'app-row-phone', templateUrl: './phone.component.html', styleUrls: ['./phone.component.css'], - imports: [CommonModule, MatFormFieldModule, MatInputModule, MatSelectModule, FormsModule] + imports: [CommonModule, MatFormFieldModule, MatInputModule, MatSelectModule, MatAutocompleteModule, FormsModule, ReactiveFormsModule] }) export class PhoneRowComponent extends BaseRowFieldComponent implements OnInit { @Input() value: string = ''; @@ -34,7 +38,10 @@ export class PhoneRowComponent extends BaseRowFieldComponent implements OnInit { selectedCountry: CountryCode; phoneNumber: string = ''; displayPhoneNumber: string = ''; - private formatter: AsYouType | null = null; + formatter: AsYouType | null = null; + + countryControl = new FormControl(null); + filteredCountries$: Observable; countries: CountryCode[] = [ { code: 'AF', name: 'Afghanistan', dialCode: '+93', flag: '๐Ÿ‡ฆ๐Ÿ‡ซ' }, @@ -287,9 +294,10 @@ export class PhoneRowComponent extends BaseRowFieldComponent implements OnInit { super.ngOnInit(); this.configureFromWidgetParams(); this.initializePhoneNumber(); + this.initializeAutocomplete(); } - private configureFromWidgetParams(): void { + configureFromWidgetParams(): void { if (this.widgetStructure && this.widgetStructure.widget_params) { const params = this.widgetStructure.widget_params; @@ -343,6 +351,7 @@ export class PhoneRowComponent extends BaseRowFieldComponent implements OnInit { const country = this.countries.find(c => c.code === phoneNumber.country); if (country) { this.selectedCountry = country; + this.countryControl.setValue(country); this.phoneNumber = phoneNumber.nationalNumber; this.displayPhoneNumber = phoneNumber.formatNational(); this.initializeFormatter(); @@ -367,10 +376,44 @@ export class PhoneRowComponent extends BaseRowFieldComponent implements OnInit { private setDefaultCountry(): void { this.selectedCountry = this.countries.find(c => c.code === this.preferredCountries[0]) || this.countries[0]; + this.countryControl.setValue(this.selectedCountry); + this.initializeFormatter(); + } + + private initializeAutocomplete(): void { + this.filteredCountries$ = this.countryControl.valueChanges.pipe( + startWith(this.selectedCountry), + map(value => { + if (typeof value === 'string') { + return this._filterCountries(value); + } else if (value && typeof value === 'object') { + return this.sortedCountries; + } + return this.sortedCountries; + }) + ); + } + + _filterCountries(value: string): CountryCode[] { + const filterValue = value.toLowerCase(); + return this.sortedCountries.filter(country => + country.name.toLowerCase().includes(filterValue) || + country.code.toLowerCase().includes(filterValue) || + country.dialCode.includes(filterValue) + ); + } + + displayCountryFn(country: CountryCode): string { + return country ? `${country.flag} ${country.name} ${country.dialCode}` : ''; + } + + onCountrySelected(country: CountryCode): void { + this.selectedCountry = country; this.initializeFormatter(); + this.formatAndUpdatePhoneNumber(); } - private initializeFormatter(): void { + initializeFormatter(): void { if (this.selectedCountry) { this.formatter = new AsYouType(this.selectedCountry.code as LibPhoneCountryCode); } @@ -423,6 +466,7 @@ export class PhoneRowComponent extends BaseRowFieldComponent implements OnInit { const detectedCountry = this.countries.find(c => c.code === phoneNumber.country); if (detectedCountry) { this.selectedCountry = detectedCountry; + this.countryControl.setValue(detectedCountry); this.phoneNumber = phoneNumber.nationalNumber; this.displayPhoneNumber = phoneNumber.formatNational(); this.initializeFormatter(); @@ -492,6 +536,155 @@ export class PhoneRowComponent extends BaseRowFieldComponent implements OnInit { return 'Enter +1234567890 or select country'; } + getExamplePhoneNumber(): string { + if (!this.selectedCountry) return ''; + + // Generate example phone number based on country + const exampleNumbers: { [key: string]: string } = { + 'US': '(202) 456-1111', + 'GB': '020 7946 0958', + 'CA': '(416) 555-1234', + 'AU': '(02) 1234 5678', + 'DE': '030 12345678', + 'FR': '01 23 45 67 89', + 'IT': '06 1234 5678', + 'ES': '91 123 45 67', + 'NL': '020 123 4567', + 'BE': '02 123 45 67', + 'CH': '044 123 45 67', + 'AT': '01 12345678', + 'SE': '08-123 456 78', + 'NO': '22 12 34 56', + 'DK': '32 12 34 56', + 'FI': '09 1234 5678', + 'PL': '12 123 45 67', + 'CZ': '224 123 456', + 'HU': '(06 1) 123 4567', + 'SK': '2 1234 5678', + 'SI': '1 123 45 67', + 'HR': '1 123 4567', + 'RO': '021 123 4567', + 'BG': '02 123 4567', + 'GR': '21 1234 5678', + 'PT': '21 123 4567', + 'IE': '01 123 4567', + 'LU': '621 123 456', + 'MT': '2123 4567', + 'CY': '22 123456', + 'EE': '372 1234', + 'LV': '2123 4567', + 'LT': '8 612 34567', + 'RU': '8 (495) 123-45-67', + 'UA': '044 123 4567', + 'BY': '8 017 123-45-67', + 'MD': '22 123456', + 'JP': '03-1234-5678', + 'KR': '02-123-4567', + 'CN': '010 1234 5678', + 'HK': '2123 4567', + 'TW': '02 1234 5678', + 'SG': '6123 4567', + 'MY': '03-1234 5678', + 'TH': '02 123 4567', + 'PH': '02 1234 5678', + 'ID': '021 1234 5678', + 'VN': '28 1234 5678', + 'IN': '011 1234 5678', + 'PK': '21 1234 5678', + 'BD': '2 1234 5678', + 'LK': '11 234 5678', + 'NP': '1 123 4567', + 'AF': '20 123 4567', + 'IR': '021 1234 5678', + 'IQ': '1 123 4567', + 'SA': '011 123 4567', + 'AE': '4 123 4567', + 'QA': '4412 3456', + 'KW': '2221 2345', + 'BH': '1712 3456', + 'OM': '2412 3456', + 'JO': '6 123 4567', + 'LB': '1 123 456', + 'SY': '11 123 4567', + 'IL': '2-123-4567', + 'PS': '59 123 4567', + 'TR': '(0212) 123 45 67', + 'GE': '32 123 45 67', + 'AM': '10 123456', + 'AZ': '12 123 45 67', + 'KZ': '8 (7172) 12 34 56', + 'KG': '312 123456', + 'TJ': '372 123456', + 'UZ': '71 123 45 67', + 'TM': '12 123456', + 'MN': '11 123456', + 'ZA': '011 123 4567', + 'EG': '02 12345678', + 'MA': '522 123456', + 'TN': '71 123 456', + 'DZ': '21 12 34 56', + 'LY': '21 123 4567', + 'SD': '15 123 4567', + 'ET': '11 123 4567', + 'KE': '20 123 4567', + 'UG': '41 123 4567', + 'TZ': '22 123 4567', + 'RW': '78 123 4567', + 'BI': '22 12 34 56', + 'DJ': '77 12 34 56', + 'SO': '1 123456', + 'ER': '1 123 456', + 'SS': '95 123 4567', + 'CF': '70 12 34 56', + 'TD': '22 12 34 56', + 'CM': '6 71 23 45 67', + 'GQ': '222 123456', + 'GA': '06 12 34 56', + 'CG': '06 612 3456', + 'CD': '12 123 4567', + 'AO': '222 123456', + 'ZM': '21 123 4567', + 'ZW': '4 123456', + 'BW': '71 123 456', + 'NA': '61 123 4567', + 'SZ': '2505 1234', + 'LS': '2212 3456', + 'MZ': '21 123456', + 'MW': '1 123 456', + 'MG': '20 12 345 67', + 'MU': '212 3456', + 'SC': '4 123 456', + 'KM': '773 1234', + 'YT': '269 61 23 45', + 'RE': '262 12 34 56', + 'MV': '330 1234', + 'BR': '(11) 1234-5678', + 'AR': '011 1234-5678', + 'CL': '2 1234 5678', + 'CO': '(601) 234 5678', + 'PE': '1 123 4567', + 'VE': '0212-1234567', + 'EC': '2 123 4567', + 'BO': '2 123 4567', + 'PY': '21 123 456', + 'UY': '2 123 4567', + 'GY': '222 1234', + 'SR': '421234', + 'GF': '594 12 34 56', + 'FK': '41234', + 'MX': '55 1234 5678', + 'GT': '2 123 4567', + 'BZ': '223 1234', + 'SV': '2123 4567', + 'HN': '2 123 4567', + 'NI': '2 123 4567', + 'CR': '2 123 4567', + 'PA': '123 4567' + }; + + return exampleNumbers[this.selectedCountry.code] || `${this.selectedCountry.dialCode} 123 4567`; + } + isValidPhoneNumber(): boolean { if (!this.phoneValidation) return true; if (!this.displayPhoneNumber) return true; // Empty is valid (let required validation handle it)