From f0958e472d32e1888ccc79767422df0980e07f70 Mon Sep 17 00:00:00 2001 From: Andrej Delmar Date: Thu, 2 Jul 2020 21:24:00 +0200 Subject: [PATCH 1/4] feat: assignments 1, 2, 5 --- cdk/cdk-workshop-stack.ts | 155 ++++++---- lib/api/pin-lambda.ts | 164 +++++++---- lib/shared/types/pin.types.ts | 35 +-- lib/web/src/app/app.module.ts | 76 ++--- lib/web/src/app/components/app.component.ts | 85 +++--- lib/web/src/app/components/image.component.ts | 62 ++-- .../app/components/image.page.component.ts | 67 +++++ lib/web/src/app/components/map.component.ts | 256 +++++++++++------ .../app/components/pin-marker.component.ts | 110 ++++---- .../components/share-dialog.component.html | 6 + .../components/share-dialog.component.scss | 12 + .../app/components/share-dialog.component.ts | 30 ++ .../src/app/components/sidebar.component.ts | 266 +++++++++++++----- lib/web/src/app/routes.ts | 14 + lib/web/src/app/services/pin-api.service.ts | 62 ++-- lib/web/src/app/state/pin.state.ts | 209 +++++++++----- package.json | 2 + 17 files changed, 1074 insertions(+), 537 deletions(-) create mode 100644 lib/web/src/app/components/image.page.component.ts create mode 100644 lib/web/src/app/components/share-dialog.component.html create mode 100644 lib/web/src/app/components/share-dialog.component.scss create mode 100644 lib/web/src/app/components/share-dialog.component.ts create mode 100644 lib/web/src/app/routes.ts diff --git a/cdk/cdk-workshop-stack.ts b/cdk/cdk-workshop-stack.ts index 39f5d0a..aa6e02e 100644 --- a/cdk/cdk-workshop-stack.ts +++ b/cdk/cdk-workshop-stack.ts @@ -1,45 +1,69 @@ -import { LambdaIntegration, RestApi } from '@aws-cdk/aws-apigateway'; -import { CloudFrontWebDistribution, CloudFrontWebDistributionProps, PriceClass } from '@aws-cdk/aws-cloudfront'; -import { CfnOutput, Construct, Duration, RemovalPolicy, Stack, StackProps } from '@aws-cdk/core'; -import { AttributeType, BillingMode, Table } from '@aws-cdk/aws-dynamodb'; -import { Code, Function, LayerVersion, Runtime } from '@aws-cdk/aws-lambda'; -import { Bucket, EventType } from '@aws-cdk/aws-s3'; -import { BucketDeployment, Source } from '@aws-cdk/aws-s3-deployment'; -import { LambdaDestination } from '@aws-cdk/aws-s3-notifications'; -import { path as rootPath } from 'app-root-path'; -import { resolve } from 'path'; - -import { addCorsOptions } from './cors.utils'; -import { WebIndex } from './web-index'; +import { LambdaIntegration, RestApi } from '@aws-cdk/aws-apigateway' +import { + CloudFrontWebDistribution, + CloudFrontWebDistributionProps, + PriceClass +} from '@aws-cdk/aws-cloudfront' +import { + CfnOutput, + Construct, + Duration, + RemovalPolicy, + Stack, + StackProps +} from '@aws-cdk/core' +import { + AttributeType, + BillingMode, + Table, + StreamViewType +} from '@aws-cdk/aws-dynamodb' +import { Code, Function, LayerVersion, Runtime } from '@aws-cdk/aws-lambda' +import { Bucket, EventType } from '@aws-cdk/aws-s3' +import { BucketDeployment, Source } from '@aws-cdk/aws-s3-deployment' +import { LambdaDestination } from '@aws-cdk/aws-s3-notifications' +import { path as rootPath } from 'app-root-path' +import { resolve } from 'path' +import { Topic } from '@aws-cdk/aws-sns' + +import { addCorsOptions } from './cors.utils' +import { WebIndex } from './web-index' export interface CdkWorkshopStackProps extends StackProps { - userName: string; + userName: string } export class CdkWorkshopStack extends Stack { - constructor(scope: Construct, id: string, props: CdkWorkshopStackProps) { - super(scope, id, props); + constructor (scope: Construct, id: string, props: CdkWorkshopStackProps) { + super(scope, id, props) // API - const imageBucket = new Bucket(this, 'ImageBucket'); + // const topic = new Topic(this, 'Topic', { + // displayName: 'Pin topic' + // }) + + const imageBucket = new Bucket(this, 'ImageBucket') const pinTable = new Table(this, 'PinTable', { + replicationRegions: [process.env.AWS_REGION as string], + stream: StreamViewType.NEW_IMAGE, partitionKey: { name: 'pointUrl', type: AttributeType.STRING }, billingMode: BillingMode.PAY_PER_REQUEST, removalPolicy: RemovalPolicy.DESTROY - }); + }) + pinTable.tableStreamArn - const apiCode = Code.fromAsset('dist/api'); + const apiCode = Code.fromAsset('dist/api') const helloHandler = new Function(this, 'HelloHandler', { code: apiCode, runtime: Runtime.NODEJS_10_X, handler: 'hello-lambda.handler' - }); + }) const pinHandler = new Function(this, 'PinHandler', { code: apiCode, @@ -49,16 +73,17 @@ export class CdkWorkshopStack extends Stack { IMAGE_BUCKET: imageBucket.bucketName, PIN_TABLE: pinTable.tableName } - }); - imageBucket.grantReadWrite(pinHandler); - pinTable.grantReadWriteData(pinHandler); + }) + // pinHandler.addEventSource(new SnsEventSource(topic)) + imageBucket.grantReadWrite(pinHandler) + pinTable.grantReadWriteData(pinHandler) const sharpLayer = new LayerVersion(this, `SharpLayer_${props.userName}`, { code: Code.fromAsset('lib/layers/sharp_layer.zip'), compatibleRuntimes: [Runtime.NODEJS_10_X], license: 'Apache-2.0', description: 'Sharp image processing library v.0.23.1' - }); + }) const thumbnailHandler = new Function(this, 'ThumbnailHandler', { code: apiCode, @@ -71,83 +96,91 @@ export class CdkWorkshopStack extends Stack { IMAGE_BUCKET: imageBucket.bucketName, PIN_TABLE: pinTable.tableName } - }); - imageBucket.grantReadWrite(thumbnailHandler); - pinTable.grantReadWriteData(thumbnailHandler); + }) + imageBucket.grantReadWrite(thumbnailHandler) + pinTable.grantReadWriteData(thumbnailHandler) // S3 integration imageBucket.addEventNotification( EventType.OBJECT_CREATED, new LambdaDestination(thumbnailHandler), { prefix: 'original' } - ); + ) - const api = new RestApi(this, `CdkWorkshopAPI_${props.userName}`); + const api = new RestApi(this, `CdkWorkshopAPI_${props.userName}`) - const helloApi = api.root.addResource('hello'); - helloApi.addMethod('GET', new LambdaIntegration(helloHandler)); + const helloApi = api.root.addResource('hello') + helloApi.addMethod('GET', new LambdaIntegration(helloHandler)) - const pinApi = api.root.addResource('pin'); + const pinApi = api.root.addResource('pin') // OPTIONS /pin - addCorsOptions(pinApi); + addCorsOptions(pinApi) // ANY /pin - pinApi.addMethod('ANY', new LambdaIntegration(pinHandler)); + pinApi.addMethod('ANY', new LambdaIntegration(pinHandler)) - const pinPointApi = pinApi.addResource('{pointUrl}'); + const pinPointApi = pinApi.addResource('{pointUrl}') // OPTIONS /pin/{pointUrl} - addCorsOptions(pinPointApi); + addCorsOptions(pinPointApi) // ANY /pin/{pointUrl} - pinPointApi.addMethod('ANY', new LambdaIntegration(pinHandler)); + pinPointApi.addMethod('ANY', new LambdaIntegration(pinHandler)) // WEB const webBucket = new Bucket(this, 'WebBucket', { websiteIndexDocument: 'index.html' - }); + }) - webBucket.grantPublicAccess(); + webBucket.grantPublicAccess() - const webSource = Source.asset(resolve(rootPath, 'dist/web')); + const webSource = Source.asset(resolve(rootPath, 'dist/web')) const webDeployment = new BucketDeployment(this, 'WebDeployment', { sources: [webSource], destinationBucket: webBucket - }); + }) const webIndex = new WebIndex(this, 'WebIndex', { apiBaseUrl: api.url, source: webSource, bucket: webBucket - }); + }) - webIndex.node.addDependency(webDeployment); + webIndex.node.addDependency(webDeployment) new CfnOutput(this, 'WebBucketUrl', { value: webBucket.bucketWebsiteUrl - }); + }) // CDN const cloudFrontProps: CloudFrontWebDistributionProps = { priceClass: PriceClass.PRICE_CLASS_100, - originConfigs: [{ - s3OriginSource: { s3BucketSource: webBucket }, - behaviors: [ - { - pathPattern: 'index.html', - defaultTtl: Duration.seconds(0), - maxTtl: Duration.seconds(0), - minTtl: Duration.seconds(0) - }, - { isDefaultBehavior: true } - ] - }] - }; - - const cloudFront = new CloudFrontWebDistribution(this, 'WebDistribution', cloudFrontProps); - - new CfnOutput(this, 'WebDistributionDomainName', { value: cloudFront.domainName }); + originConfigs: [ + { + s3OriginSource: { s3BucketSource: webBucket }, + behaviors: [ + { + pathPattern: 'index.html', + defaultTtl: Duration.seconds(0), + maxTtl: Duration.seconds(0), + minTtl: Duration.seconds(0) + }, + { isDefaultBehavior: true } + ] + } + ] + } + + const cloudFront = new CloudFrontWebDistribution( + this, + 'WebDistribution', + cloudFrontProps + ) + + new CfnOutput(this, 'WebDistributionDomainName', { + value: cloudFront.domainName + }) } } diff --git a/lib/api/pin-lambda.ts b/lib/api/pin-lambda.ts index 748d34f..0bfcf15 100644 --- a/lib/api/pin-lambda.ts +++ b/lib/api/pin-lambda.ts @@ -1,114 +1,154 @@ -import { APIGatewayProxyEvent, Context } from 'aws-lambda'; -import { DynamoDB } from 'aws-sdk'; -import { parse } from 'path'; -import { v4 } from 'uuid'; +import { APIGatewayProxyEvent, Context } from 'aws-lambda' +import { DynamoDB } from 'aws-sdk' +import { parse } from 'path' +import { v4 } from 'uuid' -import { deleteImageFromS3, resolveSignedUrl, saveImageToS3 } from './utils/s3.utils'; -import { SavedImage, SavedPin, Pin } from '../shared/types/pin.types'; -import { pointToUrl } from '../shared/utils/point.utils'; +import { + deleteImageFromS3, + resolveSignedUrl, + saveImageToS3 +} from './utils/s3.utils' +import { SavedImage, SavedPin, Pin } from '../shared/types/pin.types' +import { pointToUrl } from '../shared/utils/point.utils' -const pinTable = process.env.PIN_TABLE as string; +const pinTable = process.env.PIN_TABLE as string const dynamo = new DynamoDB.DocumentClient({ endpoint: process.env.DYNAMODB_ENDPOINT -}); +}) -export async function handler(event: APIGatewayProxyEvent, context: Context) { - context.callbackWaitsForEmptyEventLoop = false; +export async function handler (event: APIGatewayProxyEvent, context: Context) { + context.callbackWaitsForEmptyEventLoop = false - const httpMethod = event.httpMethod.toUpperCase(); - const pointUrl = event.pathParameters && event.pathParameters.pointUrl; - const sourceIp = context.identity && (context.identity as any).sourceIp; + const httpMethod = event.httpMethod.toUpperCase() + const pointUrl = event.pathParameters && event.pathParameters.pointUrl + const sourceIp = context.identity && (context.identity as any).sourceIp - console.log(`pin API: ${httpMethod}:${event.path}`, pointUrl); + console.log(`pin API: ${httpMethod}:${event.path}`, pointUrl) if (httpMethod === 'POST' && !pointUrl) { - return await handleSave(event, sourceIp); - + return await handleSave(event, sourceIp) + } else if (httpMethod === 'PATCH' && pointUrl) { + return await handleRename(event, pointUrl) } else if (httpMethod === 'GET' && !pointUrl) { - return await handleList(); - + return await handleList() } else if (httpMethod === 'GET' && pointUrl) { - return await handleGet(pointUrl); - + return await handleGet(pointUrl) } else if (httpMethod === 'DELETE' && pointUrl) { - return await handleDelete(pointUrl); - + return await handleDelete(pointUrl) } else { - return transformResult({ statusCode: 404, body: { error: 'method/path not supported'} }); + return transformResult({ + statusCode: 404, + body: { error: 'method/path not supported' } + }) } } // POST:/pin - body as Pin -async function handleSave(event: APIGatewayProxyEvent, sourceIp: string) { - const pin = JSON.parse(event.body as string) as Pin; - const { point, unsavedImage, ...pinFields } = pin; - const pointUrl = pointToUrl(pin.point); +async function handleSave (event: APIGatewayProxyEvent, sourceIp: string) { + const pin = JSON.parse(event.body as string) as Pin + const { point, unsavedImage, ...pinFields } = pin + const pointUrl = pointToUrl(pin.point) - const existingPinRecord = await getPinRecord(pointUrl); + const existingPinRecord = await getPinRecord(pointUrl) if (existingPinRecord) { - return transformResult({ statusCode: 400, body: { error: 'pin already exists' }}) + return transformResult({ + statusCode: 400, + body: { error: 'pin already exists' } + }) } - const created = Date.now(); - let savedImage: SavedImage | undefined = undefined; + const created = Date.now() + let savedImage: SavedImage | undefined = undefined if (unsavedImage) { - const { ext } = parse(unsavedImage.name); - const s3key = `original/${pointUrl}_${v4()}${ext}`; - savedImage = await saveImageToS3(s3key, unsavedImage); + const { ext } = parse(unsavedImage.name) + const s3key = `original/${pointUrl}_${v4()}${ext}` + savedImage = await saveImageToS3(s3key, unsavedImage) } - const savedPin = { pointUrl, point, sourceIp, created, ...pinFields, image: savedImage } as SavedPin; + const savedPin = { + pointUrl, + point, + sourceIp, + created, + ...pinFields, + image: savedImage + } as SavedPin + + console.log('Saving pin record', savedPin) + await dynamo.put({ TableName: pinTable, Item: savedPin }).promise() + + const savedPinWithUrl = resolveSignedUrl(savedPin) + return transformResult({ body: savedPinWithUrl }) +} - console.log('Saving pin record', savedPin); - await dynamo.put({ TableName: pinTable, Item: savedPin }).promise(); +async function handleRename (event: APIGatewayProxyEvent, pointUrl: string) { + const { customName } = JSON.parse(event.body as string) as Partial + + const params: DynamoDB.DocumentClient.UpdateItemInput = { + TableName: pinTable, + Key: { pointUrl }, + UpdateExpression: 'set #customName = :customName', + ExpressionAttributeValues: { + ':customName': customName + }, + ExpressionAttributeNames: { + '#customName': 'customName' + }, + ReturnValues: 'ALL_NEW' + } + const resp = await dynamo.update(params).promise() - const savedPinWithUrl = resolveSignedUrl(savedPin); - return transformResult({ body: savedPinWithUrl }); + const body = resolveSignedUrl(resp.Attributes as SavedPin) + return transformResult({ body }) } // GET:/pin -async function handleList() { - const result = await dynamo.scan({ TableName: pinTable }).promise(); - const pinRecords = result.Items as SavedPin[]; - return transformResult({ body: pinRecords.map(pin => resolveSignedUrl(pin)) }); +async function handleList () { + const result = await dynamo.scan({ TableName: pinTable }).promise() + const pinRecords = result.Items as SavedPin[] + return transformResult({ body: pinRecords.map(pin => resolveSignedUrl(pin)) }) } // GET:/pin/{pointUrl} -async function handleGet(pointUrl: string) { - const pinRecord = await getPinRecord(pointUrl); +async function handleGet (pointUrl: string) { + const pinRecord = await getPinRecord(pointUrl) if (!pinRecord) { - return transformResult({ statusCode: 404 }); + return transformResult({ statusCode: 404 }) } return transformResult({ body: resolveSignedUrl(pinRecord) }) } // DELETE:/pin/{pointUrl} -async function handleDelete(pointUrl: string) { - const pinRecord = await getPinRecord(pointUrl); - if (!pinRecord) return transformResult({ statusCode: 404 }); +async function handleDelete (pointUrl: string) { + const pinRecord = await getPinRecord(pointUrl) + if (!pinRecord) return transformResult({ statusCode: 404 }) - await deleteImageFromS3(pinRecord); + await deleteImageFromS3(pinRecord) - console.log('Deleting pin record: ', pointUrl); - await dynamo.delete({ TableName: pinTable, Key: { pointUrl }}) - .promise(); + console.log('Deleting pin record: ', pointUrl) + await dynamo.delete({ TableName: pinTable, Key: { pointUrl } }).promise() - return transformResult({ statusCode: 204 }); + return transformResult({ statusCode: 204 }) } // helper functions -function transformResult({ statusCode = 200, body = ''}: { statusCode?: number, body?: any } = {}) { - console.log('pin API result', statusCode, body); +function transformResult ({ + statusCode = 200, + body = '' +}: { statusCode?: number; body?: any } = {}) { + console.log('pin API result', statusCode, body) return { statusCode, - headers: { "Access-Control-Allow-Origin": "*" }, + headers: { 'Access-Control-Allow-Origin': '*' }, body: typeof body === 'string' ? body : JSON.stringify(body) - }; + } } -async function getPinRecord(pointUrl: string): Promise { - const result = await dynamo.get({ TableName: pinTable, Key: { pointUrl } }).promise(); - return result && result.Item as SavedPin; +async function getPinRecord (pointUrl: string): Promise { + const result = await dynamo + .get({ TableName: pinTable, Key: { pointUrl } }) + .promise() + return result && (result.Item as SavedPin) } diff --git a/lib/shared/types/pin.types.ts b/lib/shared/types/pin.types.ts index 7d76bb9..1fed19f 100644 --- a/lib/shared/types/pin.types.ts +++ b/lib/shared/types/pin.types.ts @@ -1,33 +1,34 @@ -import { NominatimResponse } from './nominatim.types'; +import { NominatimResponse } from './nominatim.types' export type PinPoint = { - lat: number, + lat: number lng: number } export interface Pin { - point: PinPoint; - address?: NominatimResponse; - unsavedImage?: Image; + customName: string + point: PinPoint + address?: NominatimResponse + unsavedImage?: Image } export interface Image { - name: string; - type: string; - size: number; - lastModified: number; - dataUrl?: string; + name: string + type: string + size: number + lastModified: number + dataUrl?: string } export interface SavedImage extends Image { - s3key: string; - url?: string; + s3key: string + url?: string } export interface SavedPin extends Pin { - pointUrl: string; - sourceIp: string; - created: number; - image?: SavedImage; - thumbnail?: SavedImage; + pointUrl: string + sourceIp: string + created: number + image?: SavedImage + thumbnail?: SavedImage } diff --git a/lib/web/src/app/app.module.ts b/lib/web/src/app/app.module.ts index c642e85..fdd5fe4 100644 --- a/lib/web/src/app/app.module.ts +++ b/lib/web/src/app/app.module.ts @@ -1,33 +1,37 @@ -import { HttpClientModule } from '@angular/common/http'; -import { NgModule } from '@angular/core'; -import { FormsModule, ReactiveFormsModule } from '@angular/forms'; -import { MatAutocompleteModule } from '@angular/material/autocomplete'; -import { MatButtonModule } from '@angular/material/button'; -import { MatCardModule } from '@angular/material/card'; -import { MatFormFieldModule } from '@angular/material/form-field'; -import { MatIconModule } from '@angular/material/icon'; -import { MatInputModule } from '@angular/material/input'; -import { MatProgressBarModule } from '@angular/material/progress-bar'; -import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; -import { BrowserModule, Meta } from '@angular/platform-browser'; -import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; -import { NgxsModule } from '@ngxs/store'; -import { FileSizeModule } from 'ngx-filesize'; - -import { AppComponent } from './components/app.component'; -import { HeaderComponent } from './components/header.component'; -import { ImageComponent } from './components/image.component'; -import { MapComponent } from './components/map.component'; -import { PinMarkerComponent } from './components/pin-marker.component'; -import { SearchComponent } from './components/search.component'; -import { SidebarComponent } from './components/sidebar.component'; -import { ImageInputComponent } from './components/image-input.component'; -import { Config, configFactory } from './config'; -import { NominatimService } from './services/nominatim.service'; -import { PinState } from './state/pin.state'; -import { environment } from '../environments/environment'; -import { PinApiService } from './services/pin-api.service'; +import { HttpClientModule } from '@angular/common/http' +import { NgModule } from '@angular/core' +import { FormsModule, ReactiveFormsModule } from '@angular/forms' +import { MatAutocompleteModule } from '@angular/material/autocomplete' +import { MatButtonModule } from '@angular/material/button' +import { MatCardModule } from '@angular/material/card' +import { MatFormFieldModule } from '@angular/material/form-field' +import { MatIconModule } from '@angular/material/icon' +import { MatInputModule } from '@angular/material/input' +import { MatProgressBarModule } from '@angular/material/progress-bar' +import { MatProgressSpinnerModule } from '@angular/material/progress-spinner' +import { BrowserModule, Meta } from '@angular/platform-browser' +import { BrowserAnimationsModule } from '@angular/platform-browser/animations' +import { NgxsModule } from '@ngxs/store' +import { FileSizeModule } from 'ngx-filesize' +import { AppComponent } from './components/app.component' +import { HeaderComponent } from './components/header.component' +import { ImageComponent } from './components/image.component' +import { MapComponent } from './components/map.component' +import { PinMarkerComponent } from './components/pin-marker.component' +import { SearchComponent } from './components/search.component' +import { SidebarComponent } from './components/sidebar.component' +import { ImageInputComponent } from './components/image-input.component' +import { Config, configFactory } from './config' +import { NominatimService } from './services/nominatim.service' +import { PinState } from './state/pin.state' +import { environment } from '../environments/environment' +import { PinApiService } from './services/pin-api.service' +import { ShareDialogComponent } from './components/share-dialog.component' +import { MatDialogModule } from '@angular/material/dialog' +import { RouterModule } from '@angular/router' +import { routes } from './routes' +import { ImagePageComponent } from './components/image.page.component' @NgModule({ declarations: [ @@ -38,7 +42,9 @@ import { PinApiService } from './services/pin-api.service'; MapComponent, PinMarkerComponent, SearchComponent, - SidebarComponent + SidebarComponent, + ShareDialogComponent, + ImagePageComponent ], imports: [ BrowserModule, @@ -55,18 +61,18 @@ import { PinApiService } from './services/pin-api.service'; MatInputModule, MatProgressBarModule, MatProgressSpinnerModule, + MatDialogModule, NgxsModule.forRoot([PinState], { developmentMode: !environment.production - }) + }), + RouterModule.forRoot(routes) ], providers: [ NominatimService, PinApiService, { provide: Config, useFactory: configFactory, deps: [Meta] } ], - entryComponents: [PinMarkerComponent], + entryComponents: [PinMarkerComponent, ShareDialogComponent], bootstrap: [AppComponent] }) -export class AppModule { - -} +export class AppModule {} diff --git a/lib/web/src/app/components/app.component.ts b/lib/web/src/app/components/app.component.ts index ddac0a5..273bcdd 100644 --- a/lib/web/src/app/components/app.component.ts +++ b/lib/web/src/app/components/app.component.ts @@ -1,56 +1,67 @@ -import { Component } from '@angular/core'; -import { Select } from '@ngxs/store'; -import { Observable } from 'rxjs'; +import { Component } from '@angular/core' +import { Select } from '@ngxs/store' +import { Observable } from 'rxjs' -import { PinState } from '../state/pin.state'; -import { Pin } from 'shared/types/pin.types'; +import { PinState } from '../state/pin.state' +import { Pin } from 'shared/types/pin.types' @Component({ selector: 'app-root', - styles: [` + styles: [ + ` :host { - height: 100vh; - display: flex; - flex-flow: column; + height: 100vh; + display: flex; + flex-flow: column; } .header { - flex: 0 0 48px; - height: 48px; - border-bottom: 3px solid #d0d5db; + flex: 0 0 48px; + height: 48px; + border-bottom: 3px solid #d0d5db; } .main { - height: 100%; /*calc(100% - 48px);*/ - display: flex; - flex-flow: row; - overflow-y: auto; + height: 100%; /*calc(100% - 48px);*/ + display: flex; + flex-flow: row; + overflow-y: auto; + } + .fixed { + flex: 1; } - .fixed { flex: 1 } .sidebar { - flex: 0 0 400px; - overflow-y: auto; - padding: 8px; + flex: 0 0 400px; + overflow-y: auto; + padding: 8px; } @media (max-width: 863px) { - .main { flex-flow: column; } - .fixed { flex: 0 0 360px; } - .sidebar { flex: 1 0 auto; } + .main { + flex-flow: column; + } + .fixed { + flex: 0 0 360px; + } + .sidebar { + flex: 1 0 auto; + } } - `], + ` + ], template: ` -
- +
+ +
+
+ +
+
+ +
+ -
-
-
-
- -
` +
+ ` }) export class AppComponent { - - @Select(PinState.selectedPin) selectedPin$: Observable; - + @Select(PinState.selectedPin) selectedPin$: Observable } diff --git a/lib/web/src/app/components/image.component.ts b/lib/web/src/app/components/image.component.ts index e93ce79..d96b54a 100644 --- a/lib/web/src/app/components/image.component.ts +++ b/lib/web/src/app/components/image.component.ts @@ -1,45 +1,53 @@ -import { Component, Input, OnChanges, SimpleChanges } from '@angular/core'; +import { Component, Input, OnChanges, SimpleChanges } from '@angular/core' -import { Image, SavedImage } from 'shared/types/pin.types'; +import { Image, SavedImage } from 'shared/types/pin.types' @Component({ selector: 'app-image', - styles: [` - .bottom { + styles: [ + ` + .bottom { display: flex; justify-content: space-between; padding-top: 8px; - } - img { + } + img { max-width: 100%; height: auto; - } - `], + } + ` + ], template: ` - - -
- -
-
-
{{ image.size | filesize }}
-
{{ image.lastModified | date }}
-
-
+ + +
+ +
+
+
{{ image.size | filesize }}
+
{{ image.lastModified | date }}
+
+
` }) export class ImageComponent implements OnChanges { + @Input() image: Partial + + @Input() diplayBottom: boolean = true + + loading = true - @Input() image: Partial; - - loading = true; - - ngOnChanges(changes: SimpleChanges): void { + ngOnChanges (changes: SimpleChanges): void { if (changes.image && this.image) { - this.loading = true; + this.loading = true } } - } diff --git a/lib/web/src/app/components/image.page.component.ts b/lib/web/src/app/components/image.page.component.ts new file mode 100644 index 0000000..ae9ad87 --- /dev/null +++ b/lib/web/src/app/components/image.page.component.ts @@ -0,0 +1,67 @@ +import { Component } from '@angular/core' +import { Image } from 'shared/types/pin.types' +import { ActivatedRoute, Router, Params } from '@angular/router' +import { switchMap, map, tap, catchError } from 'rxjs/operators' +import { PinApiService } from '../services/pin-api.service' +import { PinFromMap, SidebarInfo } from '../state/pin.state' +import { Store } from '@ngxs/store' +import { of, ObservableInput, Observable } from 'rxjs' +import { SavedPin, SavedImage } from '../../../../shared/types/pin.types' + +@Component({ + selector: 'app-image-page', + styles: [ + ` + .error { + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + } + ` + ], + template: ` + + +
+

No pin found

+ +
+
+ ` +}) +export class ImagePageComponent { + constructor ( + private route: ActivatedRoute, + private pinApi: PinApiService, + private store: Store, + private router: Router + ) {} + + public hasError = false + public pinUrl + + public goHome () { + this.router.navigateByUrl(`/?pointUrl=${this.pinUrl}`) + } + + public image: Observable = this.route.params.pipe( + tap(params => { + this.pinUrl = params.id + }), + switchMap>((params: Params) => + this.pinApi.getPin(params.id) + ), + tap(pin => this.store.dispatch(new PinFromMap(pin.pointUrl))), + tap(() => this.store.dispatch(new SidebarInfo(true))), + map(pin => pin.image), + catchError>(() => { + this.hasError = true + return of(null) + }) + ) +} diff --git a/lib/web/src/app/components/map.component.ts b/lib/web/src/app/components/map.component.ts index 8fd1ebd..1975bb3 100644 --- a/lib/web/src/app/components/map.component.ts +++ b/lib/web/src/app/components/map.component.ts @@ -1,18 +1,42 @@ -import { Component, HostListener, Input } from '@angular/core'; -import { ApplicationRef, ComponentFactoryResolver, ComponentRef, Injector, OnDestroy, SimpleChange } from '@angular/core'; -import { ElementRef, OnChanges, OnInit, SimpleChanges, ViewEncapsulation } from '@angular/core'; -import { Select, Store } from '@ngxs/store'; -import * as L from 'leaflet'; -import * as isEqual from 'lodash.isequal'; -import { Observable, Subscription } from 'rxjs'; -import { distinctUntilChanged, filter, first, map, withLatestFrom } from 'rxjs/operators'; - -import { PinMarkerComponent } from './pin-marker.component'; -import { PinFromMap, PinState } from '../state/pin.state'; -import { diffArrays } from '../utils/array.utils'; -import { CustomIcon } from '../utils/leaflet.utils'; -import { simpleChange } from '../utils/simple-change.operator'; -import { PinPoint, Pin, SavedPin } from 'shared/types/pin.types'; +import { Component, HostListener, Input } from '@angular/core' +import { + ApplicationRef, + ComponentFactoryResolver, + ComponentRef, + Injector, + OnDestroy, + SimpleChange +} from '@angular/core' +import { + ElementRef, + OnChanges, + OnInit, + SimpleChanges, + ViewEncapsulation +} from '@angular/core' +import { Select, Store, Actions } from '@ngxs/store' +import * as L from 'leaflet' +import * as isEqual from 'lodash.isequal' +import { Observable, Subscription, forkJoin, of } from 'rxjs' +import { + distinctUntilChanged, + filter, + first, + map, + withLatestFrom, + tap, + catchError, + switchMap +} from 'rxjs/operators' + +import { PinMarkerComponent } from './pin-marker.component' +import { PinFromMap, PinState } from '../state/pin.state' +import { diffArrays } from '../utils/array.utils' +import { CustomIcon } from '../utils/leaflet.utils' +import { simpleChange } from '../utils/simple-change.operator' +import { PinPoint, Pin, SavedPin } from 'shared/types/pin.types' +import { ActivatedRoute } from '@angular/router' +import { PinApiService } from '../services/pin-api.service' @Component({ selector: 'div[map]', // tslint:disable-line @@ -21,29 +45,31 @@ import { PinPoint, Pin, SavedPin } from 'shared/types/pin.types'; template: '' }) export class MapComponent implements OnInit, OnChanges, OnDestroy { + @Input() pin: Pin - @Input() pin: Pin; - - @Select(PinState.pins) pins$: Observable; + @Select(PinState.pins) pins$: Observable + @Select(PinState.selectedPin) selectedPin$: Observable - map: L.Map; - selectedPinMarker: L.Marker; - savedPinMarkers: {[pointUrl: string]: L.Marker} = {}; + map: L.Map + selectedPinMarker: L.Marker + savedPinMarkers: { [pointUrl: string]: L.Marker } = {} - private subscriptions = [] as Subscription[]; + private subscriptions = [] as Subscription[] - constructor( + constructor ( private injector: Injector, private resolver: ComponentFactoryResolver, private appRef: ApplicationRef, private el: ElementRef, - private store: Store + private store: Store, + private route: ActivatedRoute, + private pinApi: PinApiService ) { - L.Icon.Default.imagePath = 'assets/leaflet/'; + L.Icon.Default.imagePath = 'assets/leaflet/' } - ngOnInit(): void { - const initialCenter = [48, 17]; + ngOnInit (): void { + const initialCenter = [48, 17] this.map = L.map(this.el.nativeElement, { worldCopyJump: true, @@ -54,113 +80,179 @@ export class MapComponent implements OnInit, OnChanges, OnDestroy { zoom: 4, layers: [ new L.TileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { - 'attribution': 'Map data © OpenStreetMap contributors' + attribution: + 'Map data © OpenStreetMap contributors' }) ] - }); + }) + + this.route.queryParams + .pipe( + first(), + filter(params => !!params.pointUrl), + map(p => p.pointUrl as string), + switchMap(pointUrl => + this.pinApi.getPin(pointUrl).pipe( + map(() => pointUrl), + catchError(e => of(pointUrl)) + ) + ) + ) + .subscribe(pointUrl => { + const [lat, lng] = pointUrl.split(',').map(n => parseFloat(n)) + this.store.dispatch(new PinFromMap({ lat, lng })) + }) this.subscriptions.push( this.pins$ .pipe( - map(pins => pins.reduce((set, pin) => (set.add(pin.pointUrl), set), new Set())), + map(pins => + pins.reduce( + (set, pin) => (set.add(pin.pointUrl), set), + new Set() + ) + ), distinctUntilChanged((set1, set2) => isEqual(set1, set2)), map(pointUrlsSet => Array.from(pointUrlsSet)), simpleChange(), - map((pointUrlsChange: SimpleChange) => diffArrays( - pointUrlsChange.previousValue, - pointUrlsChange.currentValue) + map((pointUrlsChange: SimpleChange) => + diffArrays( + pointUrlsChange.previousValue, + pointUrlsChange.currentValue + ) ), withLatestFrom(this.pins$) ) .subscribe(([{ added, removed }, pins]) => { // handle removed removed.forEach(pointUrl => { - const pinMarkerToRemove = this.savedPinMarkers[pointUrl]; - this.map.removeLayer(pinMarkerToRemove); - delete this.savedPinMarkers[pointUrl]; - }); + const pinMarkerToRemove = this.savedPinMarkers[pointUrl] + this.map.removeLayer(pinMarkerToRemove) + delete this.savedPinMarkers[pointUrl] + }) // handle added - const addedPins = added.map(pointUrl => pins.find(pin => pin.pointUrl === pointUrl)); + const addedPins = added.map(pointUrl => + pins.find(pin => pin.pointUrl === pointUrl) + ) addedPins.forEach(pin => { - const pinMarkerToAdd = this.createPinMarker(pin); - this.map.addLayer(pinMarkerToAdd); - this.savedPinMarkers[pin.pointUrl] = pinMarkerToAdd; - }); + const pinMarkerToAdd = this.createPinMarker(pin) + this.map.addLayer(pinMarkerToAdd) + this.savedPinMarkers[pin.pointUrl] = pinMarkerToAdd + }) }) - ); + ) // zoom to pins - this.pins$.pipe( - filter(pins => pins && !!pins.length), - first() - ).subscribe(pins => { - const points = pins.map(pin => pin.point); - this.map.fitBounds(L.latLngBounds(points), { padding: [60, 60], maxZoom: 14 }); - }); + this.pins$ + .pipe( + filter(pins => pins && !!pins.length), + first() + ) + .subscribe(pins => { + const points = pins.map(pin => pin.point) + this.map.fitBounds(L.latLngBounds(points), { + padding: [60, 60], + maxZoom: 14 + }) + }) - this.selectedPinMarker = L.marker(initialCenter); + this.selectedPinMarker = L.marker(initialCenter) - L.control.scale().addTo(this.map); + this.subscriptions.push( + this.selectedPin$.subscribe(pin => { + this.pin = pin + if (this.map) { + if (this.pin && !(this.pin as SavedPin).pointUrl) { + // add unsaved pin marker + this.selectedPinMarker.setLatLng(this.pin.point) + if (!this.map.hasLayer(this.selectedPinMarker)) { + this.selectedPinMarker.addTo(this.map) + } + } else { + // remove unsaved pin marker + this.selectedPinMarker.removeFrom(this.map) + } + if (this.pin) { + this.ensurePointInView(this.pin.point) + } + setTimeout(() => this.map.invalidateSize({ pan: false })) + } + }) + ) + + L.control.scale().addTo(this.map) this.map.on('click', (e: L.LeafletMouseEvent) => { - this.store.dispatch(new PinFromMap(e.latlng)); - }); + this.store.dispatch(new PinFromMap(e.latlng)) + }) } - ngOnChanges(changes: SimpleChanges): void { + ngOnChanges (changes: SimpleChanges): void { if (this.map && changes.pin) { if (this.pin && !(this.pin as SavedPin).pointUrl) { // add unsaved pin marker - this.selectedPinMarker.setLatLng(this.pin.point); + this.selectedPinMarker.setLatLng(this.pin.point) if (!this.map.hasLayer(this.selectedPinMarker)) { - this.selectedPinMarker.addTo(this.map); + this.selectedPinMarker.addTo(this.map) } } else { // remove unsaved pin marker - this.selectedPinMarker.removeFrom(this.map); + this.selectedPinMarker.removeFrom(this.map) } if (this.pin) { - this.ensurePointInView(this.pin.point); + this.ensurePointInView(this.pin.point) } - setTimeout(() => this.map.invalidateSize({ pan: false })); + setTimeout(() => this.map.invalidateSize({ pan: false })) } } - ngOnDestroy(): void { - this.subscriptions.forEach(sub => sub.unsubscribe()); + ngOnDestroy (): void { + this.subscriptions.forEach(sub => sub.unsubscribe()) } @HostListener('window:resize') - onResize() { - this.map.invalidateSize(); + onResize () { + this.map.invalidateSize() } - private ensurePointInView(point: PinPoint) { + private ensurePointInView (point: PinPoint) { if (!this.map.getBounds().contains(point)) { - this.map.setView(point, this.map.getZoom()); + this.map.setView(point, this.map.getZoom()) } } - private createPinMarker(pin: SavedPin): L.Marker { - const pinMarker = L.marker(pin.point, { draggable: false, zIndexOffset: 100 }); - const icon = this.createPinMarkerIcon(pin.pointUrl); - pinMarker.setIcon(new CustomIcon({ nativeElement: icon.location.nativeElement })); - return pinMarker; + private createPinMarker (pin: SavedPin): L.Marker { + const pinMarker = L.marker(pin.point, { + draggable: false, + zIndexOffset: 100 + }) + const icon = this.createPinMarkerIcon(pin.pointUrl) + pinMarker.setIcon( + new CustomIcon({ nativeElement: icon.location.nativeElement }) + ) + return pinMarker } - public createPinMarkerIcon(pointUrl: string): ComponentRef { - const inputProviders = [{ provide: 'pointUrl', useValue: pointUrl }]; - let injector = Injector.create({ providers: inputProviders, parent: this.injector }); + public createPinMarkerIcon ( + pointUrl: string + ): ComponentRef { + const inputProviders = [{ provide: 'pointUrl', useValue: pointUrl }] + let injector = Injector.create({ + providers: inputProviders, + parent: this.injector + }) - const compFactory = this.resolver.resolveComponentFactory(PinMarkerComponent); - const pinMarkerIcon: ComponentRef = compFactory.create(injector); + const compFactory = this.resolver.resolveComponentFactory( + PinMarkerComponent + ) + const pinMarkerIcon: ComponentRef = compFactory.create( + injector + ) - this.appRef.attachView(pinMarkerIcon.hostView); + this.appRef.attachView(pinMarkerIcon.hostView) pinMarkerIcon.instance.onDestroyCallback = () => { - this.appRef.detachView(pinMarkerIcon.hostView); - }; - return pinMarkerIcon; + this.appRef.detachView(pinMarkerIcon.hostView) + } + return pinMarkerIcon } - - } diff --git a/lib/web/src/app/components/pin-marker.component.ts b/lib/web/src/app/components/pin-marker.component.ts index 2ed05c4..f57aa1f 100644 --- a/lib/web/src/app/components/pin-marker.component.ts +++ b/lib/web/src/app/components/pin-marker.component.ts @@ -1,12 +1,28 @@ -import { AfterViewInit, Component, ElementRef, HostListener, Inject, OnDestroy, OnInit, ViewChild } from '@angular/core'; -import { Select, Store } from '@ngxs/store'; -import * as isEqual from 'lodash.isequal'; -import { interval, Observable, of, Subscription } from 'rxjs'; -import { distinctUntilChanged, filter, first, map, switchMap, timeout } from 'rxjs/operators'; +import { + AfterViewInit, + Component, + ElementRef, + HostListener, + Inject, + OnDestroy, + OnInit, + ViewChild +} from '@angular/core' +import { Select, Store } from '@ngxs/store' +import * as isEqual from 'lodash.isequal' +import { interval, Observable, of, Subscription } from 'rxjs' +import { + distinctUntilChanged, + filter, + first, + map, + switchMap, + timeout +} from 'rxjs/operators' -import { PinApiService } from '../services/pin-api.service'; -import { PinFromMap, PinState } from '../state/pin.state'; -import { Pin, SavedImage, SavedPin } from 'shared/types/pin.types'; +import { PinApiService } from '../services/pin-api.service' +import { PinFromMap, PinState } from '../state/pin.state' +import { Pin, SavedImage, SavedPin } from 'shared/types/pin.types' @Component({ selector: 'app-pin-marker', @@ -14,42 +30,43 @@ import { Pin, SavedImage, SavedPin } from 'shared/types/pin.types'; templateUrl: './pin-marker.component.html' }) export class PinMarkerComponent implements OnInit, OnDestroy, AfterViewInit { + @Select(PinState.selectedPin) selectedPin$: Observable + @Select(PinState.pins) pins$: Observable - @Select(PinState.selectedPin) selectedPin$: Observable; - @Select(PinState.pins) pins$: Observable; + selected$: Observable + pin$: Observable - selected$: Observable; - pin$: Observable; - - thumbnail: SavedImage; - - subscription: Subscription; - public onDestroyCallback: () => void; + thumbnail: SavedImage + + subscription: Subscription + public onDestroyCallback: () => void @ViewChild('marker', { static: true }) - public marker: ElementRef; + public marker: ElementRef - constructor(private elm: ElementRef, - private store: Store, - private pinApi: PinApiService, - @Inject('pointUrl') private pointUrl: string) {} + constructor ( + private elm: ElementRef, + private store: Store, + private pinApi: PinApiService, + @Inject('pointUrl') private pointUrl: string + ) {} - ngOnInit() { + ngOnInit () { this.selected$ = this.selectedPin$.pipe( map(pin => pin && (pin as SavedPin).pointUrl === this.pointUrl), distinctUntilChanged() - ); + ) this.pin$ = this.pins$.pipe( map(pins => pins.find(p => p.pointUrl === this.pointUrl)), distinctUntilChanged((p1, p2) => isEqual(p1, p2)) - ); + ) const thumbnail$ = this.pin$.pipe( filter(pin => !!pin), switchMap(pin => { if (pin.thumbnail) { - return of(pin.thumbnail); + return of(pin.thumbnail) } else { return interval(500).pipe( switchMap((n: number) => this.pinApi.getPin(this.pointUrl)), @@ -60,38 +77,37 @@ export class PinMarkerComponent implements OnInit, OnDestroy, AfterViewInit { ) } }) - ); + ) this.subscription = thumbnail$.subscribe( - thumbnail => this.thumbnail = thumbnail - ); + thumbnail => (this.thumbnail = thumbnail) + ) } - ngAfterViewInit() { - this.adjustMarker(); + ngAfterViewInit () { + this.adjustMarker() } - + @HostListener('click', ['$event']) - onClick(event: MouseEvent) { - this.store.dispatch(new PinFromMap(this.pointUrl)); - event.stopPropagation(); + onClick (event: MouseEvent) { + this.store.dispatch(new PinFromMap(this.pointUrl)) + event.stopPropagation() } - - ngOnDestroy() { + + ngOnDestroy () { if (this.subscription) { - this.subscription.unsubscribe(); + this.subscription.unsubscribe() } - this.onDestroyCallback(); + this.onDestroyCallback() } - private adjustMarker() { - const arrowHeight = 8; - const alignWidth = 32; // align in px from left corner - - const parentElement = this.elm.nativeElement.parentElement; + private adjustMarker () { + const arrowHeight = 8 + const alignWidth = 32 // align in px from left corner + + const parentElement = this.elm.nativeElement.parentElement if (parentElement) { - const height = this.marker.nativeElement.offsetHeight + arrowHeight; - parentElement.style.margin = `-${height}px 0 0 -${alignWidth}px`; + const height = this.marker.nativeElement.offsetHeight + arrowHeight + parentElement.style.margin = `-${height}px 0 0 -${alignWidth}px` } } - } diff --git a/lib/web/src/app/components/share-dialog.component.html b/lib/web/src/app/components/share-dialog.component.html new file mode 100644 index 0000000..cb1973d --- /dev/null +++ b/lib/web/src/app/components/share-dialog.component.html @@ -0,0 +1,6 @@ +

Share url

+
+ +
diff --git a/lib/web/src/app/components/share-dialog.component.scss b/lib/web/src/app/components/share-dialog.component.scss new file mode 100644 index 0000000..73e4843 --- /dev/null +++ b/lib/web/src/app/components/share-dialog.component.scss @@ -0,0 +1,12 @@ +pre.share { + background-color: #e0dfdf; + padding: 10px; + border-radius: 12px; +} + +div.share { + display: flex; + flex-direction: row; + justify-content: flex-start; + cursor: pointer; +} diff --git a/lib/web/src/app/components/share-dialog.component.ts b/lib/web/src/app/components/share-dialog.component.ts new file mode 100644 index 0000000..61dd25a --- /dev/null +++ b/lib/web/src/app/components/share-dialog.component.ts @@ -0,0 +1,30 @@ +import { Component, Inject, OnInit } from '@angular/core' +import { MAT_DIALOG_DATA } from '@angular/material/dialog' + +export interface DialogData { + pointUrl: string +} + +@Component({ + selector: 'app-share-dialog', + templateUrl: 'share-dialog.component.html', + styleUrls: ['./share-dialog.component.scss'] +}) +export class ShareDialogComponent implements OnInit { + constructor (@Inject(MAT_DIALOG_DATA) private data: DialogData) {} + public shareUrl: string + + ngOnInit (): void { + const current = new URL(window.location.toString()) + this.shareUrl = `${current.origin}${current.pathname}?pointUrl=${this.data.pointUrl}` + } + + public copyToClipboard () { + const tempInput = document.createElement('input') + tempInput.value = this.shareUrl + document.body.appendChild(tempInput) + tempInput.select() + document.execCommand('copy') + document.body.removeChild(tempInput) + } +} diff --git a/lib/web/src/app/components/sidebar.component.ts b/lib/web/src/app/components/sidebar.component.ts index 2a3faad..7de9328 100644 --- a/lib/web/src/app/components/sidebar.component.ts +++ b/lib/web/src/app/components/sidebar.component.ts @@ -1,94 +1,218 @@ -import { Component, Input, OnChanges, SimpleChanges, ViewChild } from '@angular/core'; -import { Store } from '@ngxs/store'; +import { + Component, + Input, + OnChanges, + SimpleChanges, + ViewChild +} from '@angular/core' +import { Store, Select } from '@ngxs/store' -import { ImageInputComponent } from './image-input.component'; -import { DeletePin, SavePin, UnselectPin } from '../state/pin.state'; -import { PinPoint, SavedPin, Image } from 'shared/types/pin.types'; -import { toDMS } from 'shared/utils/point.utils'; +import { ImageInputComponent } from './image-input.component' +import { + DeletePin, + SavePin, + UnselectPin, + PinState, + RenamePin +} from '../state/pin.state' +import { PinPoint, SavedPin, Image } from 'shared/types/pin.types' +import { toDMS } from 'shared/utils/point.utils' +import { MatDialog } from '@angular/material/dialog' +import { ShareDialogComponent } from './share-dialog.component' +import { Observable } from 'rxjs' +import { FormGroup, FormControl } from '@angular/forms' @Component({ selector: 'app-sidebar', - styles: [` + styles: [ + ` + .form { + padding: 5px; + } .close { - position: absolute !important; - top: 0px; - right: 0px; + position: absolute !important; + top: 0px; + right: 0px; } .image { - margin-top: 16px; + margin-top: 16px; } - `], + ` + ], template: ` - - - {{ printPoint(pin.point) }} - - - - {{ pin.address?.display_name || pin.address?.error }} - -
- - - - Download Image - - -
-
- - - - - - -
-
+ + + {{ printPoint(pin.point) }} + + + + {{ pin.address?.display_name || pin.address?.error }} + + +
+ + Custom name + + + +
+
+ +

Created: {{ pin.created | date }}

+

Image

+

Name: {{ pin.image?.name }}

+

Size: {{ pin.image?.size | filesize }}

+
+
+ + + + Download Image + + + +
+ +
+ + + + + + +
+
` }) export class SidebarComponent implements OnChanges { + @Input() pin: Partial + + @Select(PinState.sidebarInfo) info$: Observable - @Input() pin: Partial; - @ViewChild(ImageInputComponent, { static: false }) - private imageInputComponent: ImageInputComponent; - - unsavedImage: Image; - - constructor(private store: Store) {} - - ngOnChanges(changes: SimpleChanges): void { + private imageInputComponent: ImageInputComponent + unsavedImage: Image + + constructor (private store: Store, private dialog: MatDialog) {} + + public pinName = new FormGroup( + { + name: new FormControl() + }, + { updateOn: 'change' } + ) + + ngOnChanges (changes: SimpleChanges): void { if (changes.pin && this.pin.pointUrl) { - this.unsavedImage = undefined; + this.unsavedImage = undefined + } + + this.pinName.setValue({ name: this.getPinName() }) + } + + rename () { + if (this.pin.pointUrl) { + this.store.dispatch(new RenamePin(this.pinName.get('name').value)) } } - - selectImage() { - this.imageInputComponent.openFileDialog(); + + selectImage () { + this.imageInputComponent.openFileDialog() + } + + unselectPin () { + this.store.dispatch(new UnselectPin()) + } + + savePinWithImage (unsavedImage: Image) { + this.unsavedImage = unsavedImage + this.store.dispatch( + new SavePin(unsavedImage, this.pinName.get('name').value) + ) } - - unselectPin() { - this.store.dispatch(new UnselectPin()); + + deletePin () { + this.store.dispatch(new DeletePin()) } - - savePinWithImage(unsavedImage: Image) { - this.unsavedImage = unsavedImage; - this.store.dispatch(new SavePin(unsavedImage)); + + share () { + const pointUrl = this.pin.pointUrl + ? this.pin.pointUrl + : `${this.pin.point.lat},${this.pin.point.lng}` + this.dialog.open(ShareDialogComponent, { + data: { pointUrl } + }) } - - deletePin() { - this.store.dispatch(new DeletePin()); + + printPoint (point: PinPoint): string { + return toDMS(point.lat) + ', ' + toDMS(point.lng) } - - printPoint(point: PinPoint): string { - return toDMS(point.lat) + ', ' + toDMS(point.lng); + + getPinName (): string { + let name = '' + // if ( + // this.pin.address && + // this.pin.address.address && + // this.pin.address.address.city + // ) { + // name = this.pin.address.address.city + // } + + name = !!this.pin.customName ? this.pin.customName : name + return name } - } diff --git a/lib/web/src/app/routes.ts b/lib/web/src/app/routes.ts new file mode 100644 index 0000000..015a736 --- /dev/null +++ b/lib/web/src/app/routes.ts @@ -0,0 +1,14 @@ +import { Routes } from '@angular/router' +import { MapComponent } from './components/map.component' +import { ImagePageComponent } from './components/image.page.component' + +export const routes: Routes = [ + { + path: '', + component: MapComponent + }, + { + path: ':id', + component: ImagePageComponent + } +] diff --git a/lib/web/src/app/services/pin-api.service.ts b/lib/web/src/app/services/pin-api.service.ts index c47dc0b..dd9bb14 100644 --- a/lib/web/src/app/services/pin-api.service.ts +++ b/lib/web/src/app/services/pin-api.service.ts @@ -1,37 +1,45 @@ -import { Injectable } from '@angular/core'; -import { HttpClient } from '@angular/common/http'; -import { Observable } from 'rxjs'; +import { Injectable } from '@angular/core' +import { HttpClient } from '@angular/common/http' +import { Observable } from 'rxjs' -import { Config } from '../config'; -import { Image, Pin, SavedPin } from 'shared/types/pin.types'; +import { Config } from '../config' +import { Image, Pin, SavedPin } from 'shared/types/pin.types' @Injectable() export class PinApiService { - - private pinApiUrl: string; - - constructor(private http: HttpClient, config: Config) { - this.pinApiUrl = config.apiBaseUrl + 'pin'; + private pinApiUrl: string + + constructor (private http: HttpClient, config: Config) { + this.pinApiUrl = config.apiBaseUrl + 'pin' } - - listPins(): Observable { - return this.http - .get(this.pinApiUrl); + + listPins (): Observable { + return this.http.get(this.pinApiUrl) } - - savePin(pin: Pin, unsavedImage: Image): Observable { - return this.http - .post(this.pinApiUrl, { ...pin, unsavedImage }); + + savePin ( + pin: Pin, + unsavedImage: Image, + customName: string + ): Observable { + return this.http.post(this.pinApiUrl, { + ...pin, + customName, + unsavedImage + }) } - - deletePin(pointUrl: string): Observable { - return this.http - .delete(this.pinApiUrl + '/' + pointUrl); + + renamePin (pointUrl: string, customName: string) { + return this.http.patch(`${this.pinApiUrl}/${pointUrl}`, { + customName + }) } - - getPin(pointUrl: string): Observable { - return this.http - .get(this.pinApiUrl + '/' + pointUrl); + + deletePin (pointUrl: string): Observable { + return this.http.delete(this.pinApiUrl + '/' + pointUrl) + } + + getPin (pointUrl: string): Observable { + return this.http.get(this.pinApiUrl + '/' + pointUrl) } - } diff --git a/lib/web/src/app/state/pin.state.ts b/lib/web/src/app/state/pin.state.ts index 4365240..58f63e2 100644 --- a/lib/web/src/app/state/pin.state.ts +++ b/lib/web/src/app/state/pin.state.ts @@ -1,20 +1,27 @@ -import { Image, Pin, PinPoint, SavedPin } from 'shared/types/pin.types'; -import { Action, NgxsOnInit, Selector, State, StateContext, Store } from '@ngxs/store'; +import { Image, Pin, PinPoint, SavedPin } from 'shared/types/pin.types' +import { + Action, + NgxsOnInit, + Selector, + State, + StateContext, + Store +} from '@ngxs/store' -import { NominatimService } from '../services/nominatim.service'; -import { PinApiService } from '../services/pin-api.service'; -import { pointToUrl } from 'shared/utils/point.utils'; +import { NominatimService } from '../services/nominatim.service' +import { PinApiService } from '../services/pin-api.service' +import { pointToUrl } from 'shared/utils/point.utils' // Actions export class PinFromMap { - static readonly type = '[pin] from map'; - constructor(public point: PinPoint | string) {} + static readonly type = '[pin] from map' + constructor (public point: PinPoint | string) {} } export class PinFromSearch { - static readonly type = '[pin] from search'; - constructor(public pin: Pin) {} + static readonly type = '[pin] from search' + constructor (public pin: Pin) {} } export class UnselectPin { @@ -22,121 +29,181 @@ export class UnselectPin { } export class SavePin { - static readonly type = '[pin] save'; - constructor(public unsavedImage: Image) {} + static readonly type = '[pin] save' + constructor (public unsavedImage: Image, public customName: string = '') {} +} + +export class RenamePin { + static readonly type = '[pin] rename' + constructor (public customName: string) {} } export class DeletePin { - static readonly type = '[pin] delete'; + static readonly type = '[pin] delete' } export class GeocodePinPoint { - static readonly type = '[pin] geocode point'; - constructor(public point) {} + static readonly type = '[pin] geocode point' + constructor (public point) {} +} + +export class SidebarInfo { + static readonly type = '[pin] sidebar info' + constructor (public justInfo) { + this.justInfo = false + } } // State model export interface PinStateModel { - pins: SavedPin[]; - selectedPin: Pin | SavedPin; - inProgress: boolean; + pins: SavedPin[] + selectedPin: Pin | SavedPin + inProgress: boolean + sidebarInfo: boolean } // Reducers + effects @State({ name: 'pin', - defaults: { pins: [], selectedPin: undefined, inProgress: false } + defaults: { + pins: [], + selectedPin: undefined, + inProgress: false, + sidebarInfo: false + } }) export class PinState implements NgxsOnInit { - - @Selector() static pins(state: PinStateModel) { - return state.pins; + @Selector() static pins (state: PinStateModel) { + return state.pins + } + + @Selector() static selectedPin (state: PinStateModel) { + return state.selectedPin } - - @Selector() static selectedPin(state: PinStateModel) { - return state.selectedPin; + + @Selector() static sidebarInfo (state: SidebarInfo) { + return state.justInfo } - - constructor( + + constructor ( private store: Store, private nominatim: NominatimService, private pinApi: PinApiService ) {} - - ngxsOnInit(ctx?: StateContext): void | any { - this.pinApi.listPins().subscribe( - pins => ctx.patchState({ pins }) - ); + + ngxsOnInit (ctx?: StateContext): void | any { + this.pinApi.listPins().subscribe(pins => ctx.patchState({ pins })) } - + @Action(PinFromMap) - pinFromMap(ctx: StateContext, { point }: PinFromMap) { - const savedPin = this.findSavedPin(ctx, point); + pinFromMap (ctx: StateContext, { point }: PinFromMap) { + const savedPin = this.findSavedPin(ctx, point) if (savedPin) { - ctx.patchState({ selectedPin: savedPin }); + ctx.patchState({ selectedPin: savedPin }) } else { - ctx.patchState({ selectedPin: { point } as Pin }); - this.store.dispatch(new GeocodePinPoint(point)); + ctx.patchState({ selectedPin: { point } as Pin }) + this.store.dispatch(new GeocodePinPoint(point)) } } - + + @Action(SidebarInfo) + sidebarInfo (ctx: StateContext, value: boolean) { + ctx.patchState({ sidebarInfo: value }) + } + @Action(PinFromSearch) - pinFromSearch(ctx: StateContext, { pin }: PinFromSearch) { - const savedPin = this.findSavedPin(ctx, pin.point); - ctx.patchState({ selectedPin: savedPin || pin }); + pinFromSearch (ctx: StateContext, { pin }: PinFromSearch) { + const savedPin = this.findSavedPin(ctx, pin.point) + ctx.patchState({ selectedPin: savedPin || pin }) } - + @Action(UnselectPin) - unselectPin(ctx: StateContext) { - ctx.patchState({ selectedPin: undefined }); + unselectPin (ctx: StateContext) { + ctx.patchState({ selectedPin: undefined }) } - + @Action(SavePin) - savePin(ctx: StateContext, { unsavedImage }: SavePin) { - const { selectedPin } = ctx.getState(); + savePin ( + ctx: StateContext, + { unsavedImage, customName }: SavePin + ) { + const { selectedPin } = ctx.getState() // is unsaved pin if (!(selectedPin as SavedPin).pointUrl) { - this.pinApi.savePin(selectedPin, unsavedImage) + this.pinApi + .savePin(selectedPin, unsavedImage, customName) .subscribe(savedPin => { - const { pins } = ctx.getState(); + const { pins } = ctx.getState() ctx.patchState({ pins: [...pins, savedPin], - selectedPin: savedPin } - ); - }); + selectedPin: savedPin + }) + }) + } + } + + @Action(RenamePin) + renamePin (ctx: StateContext, { customName }: RenamePin) { + const { selectedPin } = ctx.getState() + const pointUrl = (selectedPin as SavedPin).pointUrl + // is unsaved pin + if (pointUrl) { + this.pinApi + .renamePin((selectedPin as SavedPin).pointUrl, customName) + .subscribe(savedPin => { + const { pins } = ctx.getState() + const newPins = this.updatePins(savedPin, pins) + + ctx.patchState({ pins: newPins }) + }) } } - + @Action(DeletePin) - deletePin(ctx: StateContext) { - const { selectedPin } = ctx.getState(); - const pointUrl = (selectedPin as SavedPin).pointUrl; + deletePin (ctx: StateContext) { + const { selectedPin } = ctx.getState() + const pointUrl = (selectedPin as SavedPin).pointUrl if (pointUrl) { this.pinApi.deletePin(pointUrl).subscribe(() => { - const { pins, selectedPin } = ctx.getState(); - const { point, address } = selectedPin; + const { pins, selectedPin } = ctx.getState() + const { point, address } = selectedPin ctx.patchState({ - pins: pins.filter(pin => pin.pointUrl !== pointUrl), - selectedPin: { point, address } + pins: pins.filter(pin => pin.pointUrl !== pointUrl) }) - }); + }) } } - + @Action(GeocodePinPoint) - geocodePinPoint(ctx: StateContext, { point }: GeocodePinPoint) { + geocodePinPoint ( + ctx: StateContext, + { point }: GeocodePinPoint + ) { this.nominatim.getAddress(point).subscribe(address => { - const { selectedPin } = ctx.getState(); + const { selectedPin } = ctx.getState() ctx.patchState({ selectedPin: { ...selectedPin, address } }) - }); + }) } - - private findSavedPin(ctx: StateContext, point: PinPoint | string): SavedPin | undefined { - const pointUrl = typeof point === 'string' ? point : pointToUrl(point); - const { pins } = ctx.getState(); - return pins.find(pin => pin.pointUrl === pointUrl); + + private findSavedPin ( + ctx: StateContext, + point: PinPoint | string + ): SavedPin | undefined { + const pointUrl = typeof point === 'string' ? point : pointToUrl(point) + const { pins } = ctx.getState() + return pins.find(pin => pin.pointUrl === pointUrl) + } + + private updatePins (pin: SavedPin, pins: SavedPin[]): SavedPin[] { + const newPins = Array.from(pins) + newPins.splice( + pins.findIndex(p => p.pointUrl === pin.pointUrl), + 1, + pin + ) + + return newPins } - } diff --git a/package.json b/package.json index 9a9c055..206a4b7 100644 --- a/package.json +++ b/package.json @@ -43,6 +43,7 @@ "@angular/material": "^8.2.3", "@angular/platform-browser": "^8.2.11", "@angular/platform-browser-dynamic": "^8.2.11", + "@angular/router": "^8.2.11", "@aws-cdk/aws-apigateway": "^1.14.0", "@aws-cdk/aws-cloudformation": "^1.14.0", "@aws-cdk/aws-cloudfront": "^1.14.0", @@ -51,6 +52,7 @@ "@aws-cdk/aws-s3": "^1.14.0", "@aws-cdk/aws-s3-deployment": "^1.14.0", "@aws-cdk/aws-s3-notifications": "^1.14.0", + "@aws-cdk/aws-sns": "^1.14.0", "@aws-cdk/core": "^1.14.0", "@ngxs/store": "^3.5.1", "app-root-path": "^2.2.1", From 750dfaae819b4388953b9c9199f1860f1fc811d0 Mon Sep 17 00:00:00 2001 From: Andrej Delmar Date: Mon, 6 Jul 2020 08:22:00 +0200 Subject: [PATCH 2/4] feat: translation + streaming parts --- cdk/cdk-workshop-stack.ts | 106 ++++++++++- cdk/cdk-workshop.ts | 16 +- lib/api/pin-lambda.ts | 13 +- lib/api/pin-socket-lambda.ts | 5 + lib/shared/types/nominatim.types.ts | 61 +++--- lib/tsconfig.web.json | 4 +- lib/web/src/app/app.module.ts | 13 +- lib/web/src/app/components/app.component.ts | 2 + .../src/app/components/header.component.ts | 105 ++++++---- .../src/app/components/search.component.ts | 179 ++++++++++-------- .../components/share-dialog.component.html | 1 + .../src/app/components/sidebar.component.ts | 24 +-- lib/web/src/app/state/pin.state.ts | 8 +- lib/web/src/app/translation-loader.ts | 15 ++ lib/web/src/assets/i18n/en.json | 16 ++ lib/web/src/assets/i18n/sk.json | 16 ++ lib/webpack.api.js | 23 ++- package.json | 4 + serverless.yml | 13 +- 19 files changed, 431 insertions(+), 193 deletions(-) create mode 100644 lib/api/pin-socket-lambda.ts create mode 100644 lib/web/src/app/translation-loader.ts create mode 100644 lib/web/src/assets/i18n/en.json create mode 100644 lib/web/src/assets/i18n/sk.json diff --git a/cdk/cdk-workshop-stack.ts b/cdk/cdk-workshop-stack.ts index aa6e02e..039bd5f 100644 --- a/cdk/cdk-workshop-stack.ts +++ b/cdk/cdk-workshop-stack.ts @@ -1,4 +1,11 @@ -import { LambdaIntegration, RestApi } from '@aws-cdk/aws-apigateway' +import { + LambdaIntegration, + RestApi, + CfnIntegrationV2, + CfnRouteV2, + CfnApiV2 +} from '@aws-cdk/aws-apigateway' +// import * as gw from '@aws-cdk/aws-apigatewayv2' import { CloudFrontWebDistribution, CloudFrontWebDistributionProps, @@ -18,13 +25,29 @@ import { Table, StreamViewType } from '@aws-cdk/aws-dynamodb' -import { Code, Function, LayerVersion, Runtime } from '@aws-cdk/aws-lambda' +import { + Code, + Function, + LayerVersion, + Runtime, + StartingPosition + // EventSourceMapping +} from '@aws-cdk/aws-lambda' import { Bucket, EventType } from '@aws-cdk/aws-s3' import { BucketDeployment, Source } from '@aws-cdk/aws-s3-deployment' import { LambdaDestination } from '@aws-cdk/aws-s3-notifications' import { path as rootPath } from 'app-root-path' import { resolve } from 'path' import { Topic } from '@aws-cdk/aws-sns' +// import * as subs from '@aws-cdk/aws-sns-subscriptions' +// import { DynamoEventSource } from '@aws-cdk/aws-lambda-event-sources' + +import { + PolicyStatement, + Effect, + Role, + ServicePrincipal +} from '@aws-cdk/aws-iam' import { addCorsOptions } from './cors.utils' import { WebIndex } from './web-index' @@ -39,9 +62,10 @@ export class CdkWorkshopStack extends Stack { // API - // const topic = new Topic(this, 'Topic', { - // displayName: 'Pin topic' - // }) + const topic = new Topic(this, 'Topic', { + displayName: 'Pin topic', + topicName: 'PinTopic' + }) const imageBucket = new Bucket(this, 'ImageBucket') @@ -55,7 +79,8 @@ export class CdkWorkshopStack extends Stack { billingMode: BillingMode.PAY_PER_REQUEST, removalPolicy: RemovalPolicy.DESTROY }) - pinTable.tableStreamArn + + // topic.addSubscription( new subs.) const apiCode = Code.fromAsset('dist/api') @@ -71,10 +96,40 @@ export class CdkWorkshopStack extends Stack { handler: 'pin-lambda.handler', environment: { IMAGE_BUCKET: imageBucket.bucketName, - PIN_TABLE: pinTable.tableName + PIN_TABLE: pinTable.tableName, + PIN_TOPIC: topic.topicArn ?? 'notopic' } }) - // pinHandler.addEventSource(new SnsEventSource(topic)) + + const pinSocketFunction = new Function(this, 'PinSocket', { + code: apiCode, + runtime: Runtime.NODEJS_12_X, + handler: 'pin-socket-lambda.handler', + environment: { + PIN_STREAM: pinTable.tableName, + PIN_TOPIC: topic.topicArn + } + }) + + pinTable.grantStream(pinSocketFunction) + // pinSocketFunction.addEventSource( + // new DynamoEventSource(pinTableStream, { + // startingPosition: StartingPosition.LATEST, + // batchSize: 10 + // }) + // ) + + pinSocketFunction.addEventSourceMapping('PinStream', { + eventSourceArn: pinTable.tableStreamArn as string, + startingPosition: StartingPosition.LATEST + }) + + // new EventSourceMapping(this, 'PinStream', { + // eventSourceArn: pinTableStream.tableStreamArn, + // target: pinSocketFunction, + // startingPosition: StartingPosition.LATEST + // }) + imageBucket.grantReadWrite(pinHandler) pinTable.grantReadWriteData(pinHandler) @@ -114,6 +169,41 @@ export class CdkWorkshopStack extends Stack { const pinApi = api.root.addResource('pin') + const wsApiGw = new CfnApiV2(this, 'pinSocketGw', { + name: 'pinSocketGw', + protocolType: 'WEBSOCKET', + routeSelectionExpression: '$request.body.message' + }) + const apiId = wsApiGw.ref + + const policy = new PolicyStatement({ + effect: Effect.ALLOW, + resources: [pinHandler.functionArn], + actions: ['lambda:InvokeFunction'] + }) + + const wsRole = new Role(this, 'socketGwRole', { + assumedBy: new ServicePrincipal('apigateway.amazonaws.com') + }) + + wsRole.addToPolicy(policy) + + const wsConnectIntegreation = new CfnIntegrationV2( + this, + 'wsConnectIntegreation', + { + apiId, + integrationType: 'AWS_PROXY', + integrationUri: `arn:aws:apigateway:${process.env.AWS_REGION}:lambda:path/2015-03-31/functions/${pinHandler.functionArn}/invocations`, + credentialsArn: wsRole.roleArn + } + ) + const wsConnect = new CfnRouteV2(this, 'pinSocketConnect', { + apiId, + routeKey: '$connect', + target: `integrations${wsConnectIntegreation.ref}` + }) + // OPTIONS /pin addCorsOptions(pinApi) // ANY /pin diff --git a/cdk/cdk-workshop.ts b/cdk/cdk-workshop.ts index 65752cf..27839fd 100644 --- a/cdk/cdk-workshop.ts +++ b/cdk/cdk-workshop.ts @@ -1,12 +1,12 @@ #!/usr/bin/env node -import 'source-map-support/register'; -import { App } from '@aws-cdk/core'; -import * as IAM from 'aws-sdk/clients/iam'; +import 'source-map-support/register' +import { App } from '@aws-cdk/core' +import * as IAM from 'aws-sdk/clients/iam' -import { CdkWorkshopStack } from './cdk-workshop-stack'; +import { CdkWorkshopStack } from './cdk-workshop-stack' new IAM().getUser((err, res) => { - const userName = res.User.UserName; - const app = new App(); - new CdkWorkshopStack(app, `cdk-workshop-${userName}`, { userName }); -}); + const userName = res.User.UserName + const app = new App() + new CdkWorkshopStack(app, `cdk-workshop-${userName}`, { userName }) +}) diff --git a/lib/api/pin-lambda.ts b/lib/api/pin-lambda.ts index 0bfcf15..1d38a51 100644 --- a/lib/api/pin-lambda.ts +++ b/lib/api/pin-lambda.ts @@ -1,5 +1,5 @@ import { APIGatewayProxyEvent, Context } from 'aws-lambda' -import { DynamoDB } from 'aws-sdk' +import { DynamoDB, SNS } from 'aws-sdk' import { parse } from 'path' import { v4 } from 'uuid' @@ -17,6 +17,8 @@ const dynamo = new DynamoDB.DocumentClient({ endpoint: process.env.DYNAMODB_ENDPOINT }) +const sns = new SNS({ apiVersion: '2010-03-31' }) + export async function handler (event: APIGatewayProxyEvent, context: Context) { context.callbackWaitsForEmptyEventLoop = false @@ -25,6 +27,7 @@ export async function handler (event: APIGatewayProxyEvent, context: Context) { const sourceIp = context.identity && (context.identity as any).sourceIp console.log(`pin API: ${httpMethod}:${event.path}`, pointUrl) + console.log('debug', process.env) if (httpMethod === 'POST' && !pointUrl) { return await handleSave(event, sourceIp) @@ -79,6 +82,14 @@ async function handleSave (event: APIGatewayProxyEvent, sourceIp: string) { await dynamo.put({ TableName: pinTable, Item: savedPin }).promise() const savedPinWithUrl = resolveSignedUrl(savedPin) + console.log('topiccArn: ', process.env.PIN_TOPIC) + sns.publish( + { + TopicArn: process.env.PIN_TOPIC, + Message: JSON.stringify(savedPinWithUrl) + }, + console.log + ) return transformResult({ body: savedPinWithUrl }) } diff --git a/lib/api/pin-socket-lambda.ts b/lib/api/pin-socket-lambda.ts new file mode 100644 index 0000000..aa87f3d --- /dev/null +++ b/lib/api/pin-socket-lambda.ts @@ -0,0 +1,5 @@ +import { APIGatewayProxyEvent, Context } from 'aws-lambda' + +export function handler (event: APIGatewayProxyEvent, context: Context) { + console.log('STREAM', event) +} diff --git a/lib/shared/types/nominatim.types.ts b/lib/shared/types/nominatim.types.ts index 593cc2b..159d1ef 100644 --- a/lib/shared/types/nominatim.types.ts +++ b/lib/shared/types/nominatim.types.ts @@ -1,36 +1,37 @@ - -export type NominatimResponse = GeocodeResponse | ReverseGeocodeResponse; +export type NominatimResponse = GeocodeResponse | ReverseGeocodeResponse export interface GeocodeResponse { - readonly error?: string; - readonly place_id: string; - readonly licence: string; - readonly osm_type: string; - readonly osm_id: string; - readonly boundingbox: string[]; - readonly lat: string; - readonly lon: string; - readonly display_name: string; - readonly place_rank: string; - readonly category: string; - readonly type: string; - readonly importance: number; + readonly error?: string + readonly place_id: string + readonly licence: string + readonly osm_type: string + readonly osm_id: string + readonly boundingbox: string[] + readonly lat: string + readonly lon: string + readonly display_name: string + readonly place_rank: string + readonly category: string + readonly type: string + readonly importance: number + readonly address: any } export interface ReverseGeocodeResponse { - readonly error?: string; - readonly place_id: string; - readonly licence: string; - readonly osm_type: string; - readonly osm_id: string; - readonly boundingbox: string[]; - readonly lat: string; - readonly lon: string; - readonly display_name: string; - readonly place_rank: string; - readonly category: string; - readonly type: string; - readonly importance: number; - readonly addresstype: string; - readonly name: string; + readonly error?: string + readonly place_id: string + readonly licence: string + readonly osm_type: string + readonly osm_id: string + readonly boundingbox: string[] + readonly lat: string + readonly lon: string + readonly display_name: string + readonly place_rank: string + readonly category: string + readonly type: string + readonly importance: number + readonly addresstype: string + readonly name: string + readonly address: any } diff --git a/lib/tsconfig.web.json b/lib/tsconfig.web.json index f5d92e3..2e36d1e 100644 --- a/lib/tsconfig.web.json +++ b/lib/tsconfig.web.json @@ -12,7 +12,9 @@ "experimentalDecorators": true, "moduleResolution": "node", "importHelpers": true, - "typeRoots": ["node_modules/@types"] + "typeRoots": ["node_modules/@types"], + "resolveJsonModule": true, + "allowSyntheticDefaultImports": true }, "angularCompilerOptions": { "fullTemplateTypeCheck": true, diff --git a/lib/web/src/app/app.module.ts b/lib/web/src/app/app.module.ts index fdd5fe4..d5bfb3b 100644 --- a/lib/web/src/app/app.module.ts +++ b/lib/web/src/app/app.module.ts @@ -29,9 +29,12 @@ import { environment } from '../environments/environment' import { PinApiService } from './services/pin-api.service' import { ShareDialogComponent } from './components/share-dialog.component' import { MatDialogModule } from '@angular/material/dialog' +import { MatSelectModule } from '@angular/material/select' import { RouterModule } from '@angular/router' import { routes } from './routes' import { ImagePageComponent } from './components/image.page.component' +import { TranslateModule, TranslateLoader } from '@ngx-translate/core' +import { TranslationBasicLoader } from './translation-loader' @NgModule({ declarations: [ @@ -62,10 +65,18 @@ import { ImagePageComponent } from './components/image.page.component' MatProgressBarModule, MatProgressSpinnerModule, MatDialogModule, + MatSelectModule, NgxsModule.forRoot([PinState], { developmentMode: !environment.production }), - RouterModule.forRoot(routes) + RouterModule.forRoot(routes), + TranslateModule.forRoot({ + defaultLanguage: 'en', + loader: { + provide: TranslateLoader, + useFactory: () => new TranslationBasicLoader() + } + }) ], providers: [ NominatimService, diff --git a/lib/web/src/app/components/app.component.ts b/lib/web/src/app/components/app.component.ts index 273bcdd..4312876 100644 --- a/lib/web/src/app/components/app.component.ts +++ b/lib/web/src/app/components/app.component.ts @@ -63,5 +63,7 @@ import { Pin } from 'shared/types/pin.types' ` }) export class AppComponent { + constructor () {} + @Select(PinState.selectedPin) selectedPin$: Observable } diff --git a/lib/web/src/app/components/header.component.ts b/lib/web/src/app/components/header.component.ts index a433f89..566cdc6 100644 --- a/lib/web/src/app/components/header.component.ts +++ b/lib/web/src/app/components/header.component.ts @@ -1,59 +1,88 @@ -import { Component, HostListener, Input, OnInit } from '@angular/core'; +import { Component, HostListener, Input, OnInit, Output } from '@angular/core' +import { TranslateService } from '@ngx-translate/core' -import { Pin } from 'shared/types/pin.types'; +import { Pin } from 'shared/types/pin.types' @Component({ selector: 'app-header', - styles: [` + styles: [ + ` :host { - height: 48px; - display: flex; - align-items: center; + height: 48px; + display: flex; + align-items: center; } - a, img { - height: 24px; - padding: 0 8px; + a, + img { + height: 24px; + padding: 0 8px; } .title { - flex: 1 0 auto; - font-size: 14px; - font-weight: 500; - color: rgba(0, 0, 0, 0.87); + flex: 1 0 auto; + font-size: 14px; + font-weight: 500; + color: rgba(0, 0, 0, 0.87); } .search { - flex: 0 0 260px; - padding: 24px 16px 0px 16px; + flex: 0 0 260px; + padding: 24px 16px 0px 16px; } - `], + + .langSelect { + padding: 24px 16px 0px 16px; + margin: 3px; + } + ` + ], template: ` - - - - -
CDK FULLSTACK WORKSHOP
-
- -