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/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..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", @@ -74,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/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..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 @@ -162,6 +162,15 @@ export class DbTableWidgetsComponent implements OnInit { } `, URL: `// No settings required`, + Phone: +`// Configure international phone number widget +// example: +{ + "preferred_countries": ["US", "GB", "CA"], + "enable_placeholder": true, + "phone_validation": true +} +`, 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..88291a68b --- /dev/null +++ b/frontend/src/app/components/ui-components/row-fields/phone/phone.component.css @@ -0,0 +1,67 @@ +.phone-input-container { + display: flex; + gap: 12px; + align-items: flex-start; + 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%; + } +} + +/* Dark theme support */ +:host-context(.dark-theme) .dial-code { + color: rgba(255, 255, 255, 0.87); +} + +: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 new file mode 100644 index 000000000..0dcbdcb6d --- /dev/null +++ b/frontend/src/app/components/ui-components/row-fields/phone/phone.component.html @@ -0,0 +1,45 @@ +
+ + Country + + + + + + {{country.flag}} + {{country.dialCode}} + {{country.name}} + + + + + + + {{normalizedLabel}} + + Example: {{getExamplePhoneNumber()}} + 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 new file mode 100644 index 000000000..47732e4f9 --- /dev/null +++ b/frontend/src/app/components/ui-components/row-fields/phone/phone.component.ts @@ -0,0 +1,708 @@ +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 { 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 { + code: string; + name: string; + dialCode: string; + flag: string; +} + +@Injectable() + +@Component({ + selector: 'app-row-phone', + templateUrl: './phone.component.html', + styleUrls: ['./phone.component.css'], + imports: [CommonModule, MatFormFieldModule, MatInputModule, MatSelectModule, MatAutocompleteModule, FormsModule, ReactiveFormsModule] +}) +export class PhoneRowComponent extends BaseRowFieldComponent implements OnInit { + @Input() value: string = ''; + + static type = 'phone'; + + preferredCountries: string[] = ['US', 'GB']; + enablePlaceholder: boolean = true; + phoneValidation: boolean = true; + + selectedCountry: CountryCode; + phoneNumber: string = ''; + displayPhoneNumber: string = ''; + formatter: AsYouType | null = null; + + countryControl = new FormControl(null); + filteredCountries$: Observable; + + 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(); + this.initializePhoneNumber(); + this.initializeAutocomplete(); + } + + 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; + } + + if (typeof params.enable_placeholder === 'boolean') { + this.enablePlaceholder = params.enable_placeholder; + } + + + if (typeof params.phone_validation === 'boolean') { + this.phoneValidation = params.phone_validation; + } + } + } + + private initializePhoneNumber(): void { + if (this.value) { + this.parseExistingPhoneNumber(this.value); + } else { + // Set default country + this.setDefaultCountry(); + this.displayPhoneNumber = ''; + } + } + + private parseExistingPhoneNumber(fullNumber: string): void { + let phoneNumber; + + try { + // First try to parse as international number + phoneNumber = parsePhoneNumber(fullNumber); + } catch (error) { + // Will try with default country below + } + + // 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 (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.countryControl.setValue(country); + this.phoneNumber = phoneNumber.nationalNumber; + this.displayPhoneNumber = phoneNumber.formatNational(); + this.initializeFormatter(); + return; + } else { + console.warn('Country not found in list:', phoneNumber.country); + } + } + + // 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 { + this.displayPhoneNumber = fullNumber; + } + } + + 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(); + } + + initializeFormatter(): void { + if (this.selectedCountry) { + this.formatter = new AsYouType(this.selectedCountry.code as LibPhoneCountryCode); + } + } + + onCountryChange(): void { + this.initializeFormatter(); + this.formatAndUpdatePhoneNumber(); + } + + onPhoneNumberChange(): void { + // Check if user entered a full international number (starts with +) + if (this.displayPhoneNumber.startsWith('+')) { + this.detectCountryFromInput(); + } else { + this.formatAndUpdatePhoneNumber(); + } + } + + private formatAndUpdatePhoneNumber(): void { + if (!this.displayPhoneNumber) { + this.phoneNumber = ''; + this.value = ''; + this.onFieldChange.emit(this.value); + return; + } + + if (this.formatter && !this.displayPhoneNumber.startsWith('+')) { + this.formatter.reset(); + const formatted = this.formatter.input(this.displayPhoneNumber); + this.displayPhoneNumber = formatted; + + // Extract raw number for storage + this.phoneNumber = this.displayPhoneNumber.replace(/\D/g, ''); + } else { + 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.countryControl.setValue(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.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); + } + + 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'; + } + + 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) + + 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 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..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/yarn.lock b/yarn.lock index 563ddae5f..045c13af0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10569,6 +10569,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" @@ -12921,6 +12928,8 @@ __metadata: "root@workspace:.": version: 0.0.0-use.local resolution: "root@workspace:." + dependencies: + libphonenumber-js: ^1.12.9 languageName: unknown linkType: soft