Merge pull request #2324 from mempool/nymkappa/feature/improve-location
Create geolocation component to format geolocation data
This commit is contained in:
		
						commit
						7c1e35ae3b
					
				| @ -385,9 +385,10 @@ class NodesApi { | ||||
|   public async $getNodesPerCountry(countryId: string) { | ||||
|     try { | ||||
|       const query = ` | ||||
|       SELECT nodes.public_key, CAST(COALESCE(node_stats.capacity, 0) as INT) as capacity, CAST(COALESCE(node_stats.channels, 0) as INT) as channels, | ||||
|       nodes.alias, UNIX_TIMESTAMP(nodes.first_seen) as first_seen, UNIX_TIMESTAMP(nodes.updated_at) as updated_at, | ||||
|           geo_names_city.names as city | ||||
|         SELECT nodes.public_key, CAST(COALESCE(node_stats.capacity, 0) as INT) as capacity, CAST(COALESCE(node_stats.channels, 0) as INT) as channels, | ||||
|           nodes.alias, UNIX_TIMESTAMP(nodes.first_seen) as first_seen, UNIX_TIMESTAMP(nodes.updated_at) as updated_at, | ||||
|           geo_names_city.names as city, geo_names_country.names as country, | ||||
|           geo_names_iso.names as iso_code, geo_names_subdivision.names as subdivision | ||||
|         FROM node_stats | ||||
|         JOIN ( | ||||
|           SELECT public_key, MAX(added) as last_added | ||||
| @ -395,15 +396,19 @@ class NodesApi { | ||||
|           GROUP BY public_key | ||||
|         ) as b ON b.public_key = node_stats.public_key AND b.last_added = node_stats.added | ||||
|         RIGHT JOIN nodes ON nodes.public_key = node_stats.public_key | ||||
|         JOIN geo_names geo_names_country ON geo_names_country.id = nodes.country_id AND geo_names_country.type = 'country' | ||||
|         LEFT JOIN geo_names geo_names_country ON geo_names_country.id = nodes.country_id AND geo_names_country.type = 'country' | ||||
|         LEFT JOIN geo_names geo_names_city ON geo_names_city.id = nodes.city_id AND geo_names_city.type = 'city' | ||||
|         LEFT JOIN geo_names geo_names_iso ON geo_names_iso.id = nodes.country_id AND geo_names_iso.type = 'country_iso_code' | ||||
|         LEFT JOIN geo_names geo_names_subdivision on geo_names_subdivision.id = nodes.subdivision_id AND geo_names_subdivision.type = 'division' | ||||
|         WHERE geo_names_country.id = ? | ||||
|         ORDER BY capacity DESC | ||||
|       `;
 | ||||
| 
 | ||||
|       const [rows]: any = await DB.query(query, [countryId]); | ||||
|       for (let i = 0; i < rows.length; ++i) { | ||||
|         rows[i].country = JSON.parse(rows[i].country); | ||||
|         rows[i].city = JSON.parse(rows[i].city); | ||||
|         rows[i].subdivision = JSON.parse(rows[i].subdivision); | ||||
|       } | ||||
|       return rows; | ||||
|     } catch (e) { | ||||
| @ -417,7 +422,8 @@ class NodesApi { | ||||
|       const query = ` | ||||
|         SELECT nodes.public_key, CAST(COALESCE(node_stats.capacity, 0) as INT) as capacity, CAST(COALESCE(node_stats.channels, 0) as INT) as channels, | ||||
|           nodes.alias, UNIX_TIMESTAMP(nodes.first_seen) as first_seen, UNIX_TIMESTAMP(nodes.updated_at) as updated_at, | ||||
|           geo_names_city.names as city, geo_names_country.names as country | ||||
|           geo_names_city.names as city, geo_names_country.names as country, | ||||
|           geo_names_iso.names as iso_code, geo_names_subdivision.names as subdivision | ||||
|         FROM node_stats | ||||
|         JOIN ( | ||||
|           SELECT public_key, MAX(added) as last_added | ||||
| @ -425,8 +431,10 @@ class NodesApi { | ||||
|           GROUP BY public_key | ||||
|         ) as b ON b.public_key = node_stats.public_key AND b.last_added = node_stats.added | ||||
|         RIGHT JOIN nodes ON nodes.public_key = node_stats.public_key | ||||
|         JOIN geo_names geo_names_country ON geo_names_country.id = nodes.country_id AND geo_names_country.type = 'country' | ||||
|         LEFT JOIN geo_names geo_names_country ON geo_names_country.id = nodes.country_id AND geo_names_country.type = 'country' | ||||
|         LEFT JOIN geo_names geo_names_city ON geo_names_city.id = nodes.city_id AND geo_names_city.type = 'city' | ||||
|         LEFT JOIN geo_names geo_names_iso ON geo_names_iso.id = nodes.country_id AND geo_names_iso.type = 'country_iso_code' | ||||
|         LEFT JOIN geo_names geo_names_subdivision on geo_names_subdivision.id = nodes.subdivision_id AND geo_names_subdivision.type = 'division' | ||||
|         WHERE nodes.as_number IN (?) | ||||
|         ORDER BY capacity DESC | ||||
|       `;
 | ||||
| @ -435,6 +443,7 @@ class NodesApi { | ||||
|       for (let i = 0; i < rows.length; ++i) { | ||||
|         rows[i].country = JSON.parse(rows[i].country); | ||||
|         rows[i].city = JSON.parse(rows[i].city); | ||||
|         rows[i].subdivision = JSON.parse(rows[i].subdivision); | ||||
|       } | ||||
|       return rows; | ||||
|     } catch (e) { | ||||
|  | ||||
| @ -4,7 +4,7 @@ import { Observable } from 'rxjs'; | ||||
| import { catchError, map, switchMap } from 'rxjs/operators'; | ||||
| import { SeoService } from 'src/app/services/seo.service'; | ||||
| import { OpenGraphService } from 'src/app/services/opengraph.service'; | ||||
| import { getFlagEmoji } from 'src/app/shared/graphs.utils'; | ||||
| import { getFlagEmoji } from 'src/app/shared/common.utils'; | ||||
| import { LightningApiService } from '../lightning-api.service'; | ||||
| import { isMobile } from '../../shared/common.utils'; | ||||
| 
 | ||||
|  | ||||
| @ -42,24 +42,10 @@ | ||||
|                 <app-fiat [value]="node.avgCapacity" digitsInfo="1.0-0"></app-fiat> | ||||
|               </td> | ||||
|             </tr> | ||||
|             <tr *ngIf="node.country && node.city && node.subdivision"> | ||||
|             <tr *ngIf="node.geolocation"> | ||||
|               <td i18n="location">Location</td> | ||||
|               <td> | ||||
|                 <span>{{ node.city.en }}, {{ node.subdivision.en }}</span> | ||||
|                 <br> | ||||
|                 <a class="d-flex align-items-center" [routerLink]="['/lightning/nodes/country' | relativeUrl, node.iso_code]"> | ||||
|                   <span class="link">{{ node.country.en }}</span> | ||||
|                     | ||||
|                   <span class="flag">{{ node.flag }}</span> | ||||
|                 </a> | ||||
|               </td> | ||||
|             </tr> | ||||
|             <tr *ngIf="node.country && !node.city"> | ||||
|               <td i18n="location">Location</td> | ||||
|               <td> | ||||
|                 <a [routerLink]="['/lightning/nodes/country' | relativeUrl, node.iso_code]"> | ||||
|                   {{ node.country.en }} {{ node.flag }} | ||||
|                 </a> | ||||
|                 <app-geolocation [data]="node.geolocation" [type]="'node'"></app-geolocation> | ||||
|               </td> | ||||
|             </tr> | ||||
|           </tbody> | ||||
|  | ||||
| @ -3,9 +3,9 @@ import { ActivatedRoute, ParamMap } from '@angular/router'; | ||||
| import { Observable } from 'rxjs'; | ||||
| import { catchError, map, switchMap } from 'rxjs/operators'; | ||||
| import { SeoService } from 'src/app/services/seo.service'; | ||||
| import { getFlagEmoji } from 'src/app/shared/graphs.utils'; | ||||
| import { LightningApiService } from '../lightning-api.service'; | ||||
| import { isMobile } from '../../shared/common.utils'; | ||||
| import { GeolocationData } from 'src/app/shared/components/geolocation/geolocation.component'; | ||||
| 
 | ||||
| @Component({ | ||||
|   selector: 'app-node', | ||||
| @ -58,7 +58,6 @@ export class NodeComponent implements OnInit { | ||||
|             } else if (socket.indexOf('onion') > -1) { | ||||
|               label = 'Tor'; | ||||
|             } | ||||
|             node.flag = getFlagEmoji(node.iso_code); | ||||
|             socketsObject.push({ | ||||
|               label: label, | ||||
|               socket: node.public_key + '@' + socket, | ||||
| @ -66,6 +65,19 @@ export class NodeComponent implements OnInit { | ||||
|           } | ||||
|           node.socketsObject = socketsObject; | ||||
|           node.avgCapacity = node.capacity / Math.max(1, node.active_channel_count); | ||||
| 
 | ||||
|           if (!node?.country && !node?.city && | ||||
|             !node?.subdivision && !node?.iso) { | ||||
|               node.geolocation = null; | ||||
|           } else { | ||||
|             node.geolocation = <GeolocationData>{ | ||||
|               country: node.country?.en, | ||||
|               city: node.city?.en, | ||||
|               subdivision: node.subdivision?.en, | ||||
|               iso: node.iso_code, | ||||
|             }; | ||||
|           } | ||||
| 
 | ||||
|           return node; | ||||
|         }), | ||||
|         catchError(err => { | ||||
|  | ||||
| @ -9,7 +9,7 @@ import { StateService } from 'src/app/services/state.service'; | ||||
| import { download } from 'src/app/shared/graphs.utils'; | ||||
| import { AmountShortenerPipe } from 'src/app/shared/pipes/amount-shortener.pipe'; | ||||
| import { RelativeUrlPipe } from 'src/app/shared/pipes/relative-url/relative-url.pipe'; | ||||
| import { getFlagEmoji } from 'src/app/shared/graphs.utils'; | ||||
| import { getFlagEmoji } from 'src/app/shared/common.utils'; | ||||
| 
 | ||||
| @Component({ | ||||
|   selector: 'app-nodes-per-country-chart', | ||||
|  | ||||
| @ -36,7 +36,7 @@ | ||||
|             {{ node.channels }} | ||||
|           </td> | ||||
|           <td class="city text-right text-truncate"> | ||||
|             {{ node?.city?.en ?? '-' }} | ||||
|             <app-geolocation [data]="node.geolocation" [type]="'list-country'"></app-geolocation> | ||||
|           </td> | ||||
|       </tbody> | ||||
|     </table> | ||||
|  | ||||
| @ -3,7 +3,8 @@ import { ActivatedRoute } from '@angular/router'; | ||||
| import { map, Observable } from 'rxjs'; | ||||
| import { ApiService } from 'src/app/services/api.service'; | ||||
| import { SeoService } from 'src/app/services/seo.service'; | ||||
| import { getFlagEmoji } from 'src/app/shared/graphs.utils'; | ||||
| import { getFlagEmoji } from 'src/app/shared/common.utils'; | ||||
| import { GeolocationData } from 'src/app/shared/components/geolocation/geolocation.component'; | ||||
| 
 | ||||
| @Component({ | ||||
|   selector: 'app-nodes-per-country', | ||||
| @ -29,6 +30,16 @@ export class NodesPerCountry implements OnInit { | ||||
|             name: response.country.en, | ||||
|             flag: getFlagEmoji(this.route.snapshot.params.country) | ||||
|           }; | ||||
| 
 | ||||
|           for (const i in response.nodes) { | ||||
|             response.nodes[i].geolocation = <GeolocationData>{ | ||||
|               country: response.nodes[i].country?.en, | ||||
|               city: response.nodes[i].city?.en, | ||||
|               subdivision: response.nodes[i].subdivision?.en, | ||||
|               iso: response.nodes[i].iso_code, | ||||
|             }; | ||||
|           } | ||||
| 
 | ||||
|           this.seoService.setTitle($localize`Lightning nodes in ${this.country.name}`); | ||||
|           return response.nodes; | ||||
|         }) | ||||
|  | ||||
| @ -33,7 +33,7 @@ | ||||
|             {{ node.channels }} | ||||
|           </td> | ||||
|           <td class="city text-right text-truncate"> | ||||
|             {{ node?.city?.en ?? '-' }} | ||||
|             <app-geolocation [data]="node.geolocation" [type]="'list-isp'"></app-geolocation> | ||||
|           </td> | ||||
|       </tbody> | ||||
|     </table> | ||||
|  | ||||
| @ -3,6 +3,7 @@ import { ActivatedRoute } from '@angular/router'; | ||||
| import { map, Observable } from 'rxjs'; | ||||
| import { ApiService } from 'src/app/services/api.service'; | ||||
| import { SeoService } from 'src/app/services/seo.service'; | ||||
| import { GeolocationData } from 'src/app/shared/components/geolocation/geolocation.component'; | ||||
| 
 | ||||
| @Component({ | ||||
|   selector: 'app-nodes-per-isp', | ||||
| @ -29,6 +30,16 @@ export class NodesPerISP implements OnInit { | ||||
|             id: this.route.snapshot.params.isp | ||||
|           }; | ||||
|           this.seoService.setTitle($localize`Lightning nodes on ISP: ${response.isp} [AS${this.route.snapshot.params.isp}]`); | ||||
| 
 | ||||
|           for (const i in response.nodes) { | ||||
|             response.nodes[i].geolocation = <GeolocationData>{ | ||||
|               country: response.nodes[i].country?.en, | ||||
|               city: response.nodes[i].city?.en, | ||||
|               subdivision: response.nodes[i].subdivision?.en, | ||||
|               iso: response.nodes[i].iso_code, | ||||
|             }; | ||||
|           } | ||||
| 
 | ||||
|           return response.nodes; | ||||
|         }) | ||||
|       ); | ||||
|  | ||||
| @ -1,3 +1,120 @@ | ||||
| export function isMobile() { | ||||
| export function isMobile(): boolean { | ||||
|   return (window.innerWidth <= 767.98); | ||||
| } | ||||
| 
 | ||||
| export function getFlagEmoji(countryCode): string { | ||||
|   if (!countryCode) { | ||||
|     return ''; | ||||
|   } | ||||
|   const codePoints = countryCode | ||||
|     .toUpperCase() | ||||
|     .split('') | ||||
|     .map(char => 127397 + char.charCodeAt()); | ||||
|   return String.fromCodePoint(...codePoints); | ||||
| } | ||||
| 
 | ||||
| // https://gist.github.com/calebgrove/c285a9510948b633aa47
 | ||||
| export function convertRegion(input, to: 'name' | 'abbreviated'): string { | ||||
|   if (!input) { | ||||
|     return ''; | ||||
|   } | ||||
| 
 | ||||
|   const states = [ | ||||
|     ['Alabama', 'AL'], | ||||
|     ['Alaska', 'AK'], | ||||
|     ['American Samoa', 'AS'], | ||||
|     ['Arizona', 'AZ'], | ||||
|     ['Arkansas', 'AR'], | ||||
|     ['Armed Forces Americas', 'AA'], | ||||
|     ['Armed Forces Europe', 'AE'], | ||||
|     ['Armed Forces Pacific', 'AP'], | ||||
|     ['California', 'CA'], | ||||
|     ['Colorado', 'CO'], | ||||
|     ['Connecticut', 'CT'], | ||||
|     ['Delaware', 'DE'], | ||||
|     ['District Of Columbia', 'DC'], | ||||
|     ['Florida', 'FL'], | ||||
|     ['Georgia', 'GA'], | ||||
|     ['Guam', 'GU'], | ||||
|     ['Hawaii', 'HI'], | ||||
|     ['Idaho', 'ID'], | ||||
|     ['Illinois', 'IL'], | ||||
|     ['Indiana', 'IN'], | ||||
|     ['Iowa', 'IA'], | ||||
|     ['Kansas', 'KS'], | ||||
|     ['Kentucky', 'KY'], | ||||
|     ['Louisiana', 'LA'], | ||||
|     ['Maine', 'ME'], | ||||
|     ['Marshall Islands', 'MH'], | ||||
|     ['Maryland', 'MD'], | ||||
|     ['Massachusetts', 'MA'], | ||||
|     ['Michigan', 'MI'], | ||||
|     ['Minnesota', 'MN'], | ||||
|     ['Mississippi', 'MS'], | ||||
|     ['Missouri', 'MO'], | ||||
|     ['Montana', 'MT'], | ||||
|     ['Nebraska', 'NE'], | ||||
|     ['Nevada', 'NV'], | ||||
|     ['New Hampshire', 'NH'], | ||||
|     ['New Jersey', 'NJ'], | ||||
|     ['New Mexico', 'NM'], | ||||
|     ['New York', 'NY'], | ||||
|     ['North Carolina', 'NC'], | ||||
|     ['North Dakota', 'ND'], | ||||
|     ['Northern Mariana Islands', 'NP'], | ||||
|     ['Ohio', 'OH'], | ||||
|     ['Oklahoma', 'OK'], | ||||
|     ['Oregon', 'OR'], | ||||
|     ['Pennsylvania', 'PA'], | ||||
|     ['Puerto Rico', 'PR'], | ||||
|     ['Rhode Island', 'RI'], | ||||
|     ['South Carolina', 'SC'], | ||||
|     ['South Dakota', 'SD'], | ||||
|     ['Tennessee', 'TN'], | ||||
|     ['Texas', 'TX'], | ||||
|     ['US Virgin Islands', 'VI'], | ||||
|     ['Utah', 'UT'], | ||||
|     ['Vermont', 'VT'], | ||||
|     ['Virginia', 'VA'], | ||||
|     ['Washington', 'WA'], | ||||
|     ['West Virginia', 'WV'], | ||||
|     ['Wisconsin', 'WI'], | ||||
|     ['Wyoming', 'WY'], | ||||
|   ]; | ||||
| 
 | ||||
|   // So happy that Canada and the US have distinct abbreviations
 | ||||
|   const provinces = [ | ||||
|     ['Alberta', 'AB'], | ||||
|     ['British Columbia', 'BC'], | ||||
|     ['Manitoba', 'MB'], | ||||
|     ['New Brunswick', 'NB'], | ||||
|     ['Newfoundland', 'NF'], | ||||
|     ['Northwest Territory', 'NT'], | ||||
|     ['Nova Scotia', 'NS'], | ||||
|     ['Nunavut', 'NU'], | ||||
|     ['Ontario', 'ON'], | ||||
|     ['Prince Edward Island', 'PE'], | ||||
|     ['Quebec', 'QC'], | ||||
|     ['Saskatchewan', 'SK'], | ||||
|     ['Yukon', 'YT'], | ||||
|   ]; | ||||
| 
 | ||||
|   const regions = states.concat(provinces); | ||||
| 
 | ||||
|   let i; // Reusable loop variable
 | ||||
|   if (to == 'abbreviated') { | ||||
|     input = input.replace(/\w\S*/g, function (txt) { return txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase(); }); | ||||
|     for (i = 0; i < regions.length; i++) { | ||||
|       if (regions[i][0] == input) { | ||||
|         return (regions[i][1]); | ||||
|       } | ||||
|     } | ||||
|   } else if (to == 'name') { | ||||
|     input = input.toUpperCase(); | ||||
|     for (i = 0; i < regions.length; i++) { | ||||
|       if (regions[i][1] == input) { | ||||
|         return (regions[i][0]); | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | ||||
| @ -0,0 +1 @@ | ||||
| <span [innerHTML]="formattedLocation"></span> | ||||
| @ -0,0 +1,83 @@ | ||||
| import { Component, Input, OnChanges } from '@angular/core'; | ||||
| import { convertRegion, getFlagEmoji } from '../../common.utils'; | ||||
| 
 | ||||
| export interface GeolocationData { | ||||
|   country: string; | ||||
|   city: string; | ||||
|   subdivision: string; | ||||
|   iso: string; | ||||
| } | ||||
| 
 | ||||
| @Component({ | ||||
|   selector: 'app-geolocation', | ||||
|   templateUrl: './geolocation.component.html', | ||||
|   styleUrls: ['./geolocation.component.scss'] | ||||
| }) | ||||
| export class GeolocationComponent implements OnChanges { | ||||
|   @Input() data: GeolocationData; | ||||
|   @Input() type: 'node' | 'list-isp' | 'list-country'; | ||||
| 
 | ||||
|   formattedLocation: string = ''; | ||||
| 
 | ||||
|   ngOnChanges(): void { | ||||
|     const city = this.data.city ? this.data.city : ''; | ||||
|     const subdivisionLikeCity = this.data.city === this.data.subdivision; | ||||
|     let subdivision = this.data.subdivision; | ||||
| 
 | ||||
|     if (['US', 'CA'].includes(this.data.iso) === false || (this.type === 'node' && subdivisionLikeCity)) { | ||||
|       this.data.subdivision = undefined; | ||||
|     } else if (['list-isp', 'list-country'].includes(this.type) === true) { | ||||
|       subdivision = convertRegion(this.data.subdivision, 'abbreviated'); | ||||
|     } | ||||
| 
 | ||||
|     if (this.type === 'list-country') { | ||||
|       if (this.data.city) { | ||||
|         this.formattedLocation += ' ' + city; | ||||
|         if (this.data.subdivision) { | ||||
|           this.formattedLocation += ', ' + subdivision; | ||||
|         } | ||||
|       } else { | ||||
|         this.formattedLocation += '-'; | ||||
|       } | ||||
|     } | ||||
| 
 | ||||
|     if (this.type === 'list-isp') { | ||||
|       this.formattedLocation = getFlagEmoji(this.data.iso); | ||||
|       if (this.data.city) { | ||||
|         this.formattedLocation += ' ' + city; | ||||
|         if (this.data.subdivision) { | ||||
|           this.formattedLocation += ', ' + subdivision; | ||||
|         } | ||||
|       } else { | ||||
|         this.formattedLocation += ' ' + this.data.country; | ||||
|       } | ||||
|     } | ||||
|      | ||||
|     if (this.type === 'node') { | ||||
|       const city = this.data.city ? this.data.city : ''; | ||||
| 
 | ||||
|       // City
 | ||||
|       this.formattedLocation = `${city}`; | ||||
| 
 | ||||
|       // ,Subdivision
 | ||||
|       if (this.formattedLocation.length > 0 && !subdivisionLikeCity) { | ||||
|         this.formattedLocation += ', '; | ||||
|       } | ||||
|       if (!subdivisionLikeCity) { | ||||
|         this.formattedLocation += `${subdivision}`; | ||||
|       } | ||||
| 
 | ||||
|       // <br>[flag] County
 | ||||
|       if (this.data?.country.length ?? 0 > 0) { | ||||
|         if ((this.formattedLocation?.length ?? 0 > 0) && !subdivisionLikeCity) { | ||||
|           this.formattedLocation += '<br>'; | ||||
|         } else if (this.data.city) { | ||||
|           this.formattedLocation += ', '; | ||||
|         } | ||||
|         this.formattedLocation += `${this.data.country} ${getFlagEmoji(this.data.iso)}`; | ||||
|       } | ||||
| 
 | ||||
|       return; | ||||
|     } | ||||
|   } | ||||
| } | ||||
| @ -91,13 +91,3 @@ export function detectWebGL() { | ||||
|   return (gl && gl instanceof WebGLRenderingContext); | ||||
| } | ||||
| 
 | ||||
| export function getFlagEmoji(countryCode) { | ||||
|   if (!countryCode) { | ||||
|     return ''; | ||||
|   } | ||||
|   const codePoints = countryCode | ||||
|     .toUpperCase() | ||||
|     .split('') | ||||
|     .map(char =>  127397 + char.charCodeAt()); | ||||
|   return String.fromCodePoint(...codePoints); | ||||
| } | ||||
|  | ||||
| @ -82,6 +82,7 @@ import { SatsComponent } from './components/sats/sats.component'; | ||||
| import { SearchResultsComponent } from '../components/search-form/search-results/search-results.component'; | ||||
| import { TimestampComponent } from './components/timestamp/timestamp.component'; | ||||
| import { ToggleComponent } from './components/toggle/toggle.component'; | ||||
| import { GeolocationComponent } from '../shared/components/geolocation/geolocation.component'; | ||||
| 
 | ||||
| @NgModule({ | ||||
|   declarations: [ | ||||
| @ -158,6 +159,7 @@ import { ToggleComponent } from './components/toggle/toggle.component'; | ||||
|     SearchResultsComponent, | ||||
|     TimestampComponent, | ||||
|     ToggleComponent, | ||||
|     GeolocationComponent, | ||||
|   ], | ||||
|   imports: [ | ||||
|     CommonModule, | ||||
| @ -261,6 +263,7 @@ import { ToggleComponent } from './components/toggle/toggle.component'; | ||||
|     SearchResultsComponent, | ||||
|     TimestampComponent, | ||||
|     ToggleComponent, | ||||
|     GeolocationComponent, | ||||
|   ] | ||||
| }) | ||||
| export class SharedModule { | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user