Skip to content

Commit

Permalink
Merge pull request #124 from uatisdeproblem/103-home-communications-oage
Browse files Browse the repository at this point in the history
feat: Add home/communications page #103
  • Loading branch information
rbento1096 authored Mar 14, 2024
2 parents b4d07cc + 49b320c commit 4d8bcd1
Show file tree
Hide file tree
Showing 25 changed files with 1,174 additions and 58 deletions.
1 change: 1 addition & 0 deletions back-end/deploy/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ const apiResources: ResourceController[] = [
{ name: 'cognito', paths: ['/cognito'] },
{ name: 'galaxy', paths: ['/galaxy'] },
{ name: 'configurations', paths: ['/configurations'] },
{ name: 'media', paths: ['/media'] },
{ name: 'users', paths: ['/users', '/users/{userId}'] },
{ name: 'eventSpots', paths: ['/event-spots', '/event-spots/{spotId}'] },
{ name: 'usefulLinks', paths: ['/useful-links', '/useful-links/{linkId}'] },
Expand Down
55 changes: 55 additions & 0 deletions back-end/src/handlers/media.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
///
/// IMPORTS
///

import { DynamoDB, HandledError, ResourceController, S3 } from 'idea-aws';
import { SignedURL } from 'idea-toolbox';

import { User } from '../models/user.model';

///
/// CONSTANTS, ENVIRONMENT VARIABLES, HANDLER
///

const PROJECT = process.env.PROJECT;
const ddb = new DynamoDB();

const DDB_TABLES = { users: process.env.DDB_TABLE_users };

const S3_BUCKET_MEDIA = process.env.S3_BUCKET_MEDIA;
const S3_IMAGES_FOLDER = process.env.S3_IMAGES_FOLDER;
const s3 = new S3();

export const handler = (ev: any, _: any, cb: any): Promise<void> => new Media(ev, cb).handleRequest();

///
/// RESOURCE CONTROLLER
///

class Media extends ResourceController {
user: User;

constructor(event: any, callback: any) {
super(event, callback);
}

protected async checkAuthBeforeRequest(): Promise<void> {
try {
this.user = new User(await ddb.get({ TableName: DDB_TABLES.users, Key: { userId: this.principalId } }));
} catch (err) {
throw new HandledError('User not found');
}

if (!this.user.permissions.isAdmin) throw new HandledError('Unauthorized');
}

protected async postResources(): Promise<SignedURL> {
const imageURI = await ddb.IUNID(PROJECT.concat('-media'));

const key = `${S3_IMAGES_FOLDER}/${imageURI}.png`;
const signedURL = await s3.signedURLPut(S3_BUCKET_MEDIA, key);
signedURL.id = imageURI;

return signedURL;
}
}
10 changes: 10 additions & 0 deletions back-end/swagger.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,16 @@ paths:
$ref: '#/components/responses/OperationCompleted'
400:
$ref: '#/components/responses/BadParameters'
/media:
post:
summary: Get a signed URL to upload a new image
description: Requires to be Administrator
tags: [Configurations]
security:
- AuthFunction: []
responses:
200:
$ref: '#/components/responses/OperationCompleted'
/configurations:
get:
summary: Get the platform's configurations
Expand Down
7 changes: 6 additions & 1 deletion front-end/angular.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,12 @@
"inlineStyleLanguage": "scss",
"assets": [
{ "glob": "**/*", "input": "src/assets", "output": "assets" },
{ "glob": "**/*.svg", "input": "node_modules/ionicons/dist/ionicons/svg", "output": "./svg" }
{ "glob": "**/*.svg", "input": "node_modules/ionicons/dist/ionicons/svg", "output": "./svg" },
{
"glob": "**/*",
"input": "./node_modules/@kolkov/angular-editor/assets/",
"output": "./assets/fonts/"
}
],
"styles": ["src/theme/variables.scss", "src/global.scss"],
"scripts": [],
Expand Down
19 changes: 19 additions & 0 deletions front-end/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions front-end/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,9 @@
"@idea-ionic/common": "^7.2.15",
"@ionic/angular": "^7.7.3",
"@ionic/storage-angular": "^4.0.0",
"@kolkov/angular-editor": "^3.0.0-beta.0",
"@swimlane/ngx-datatable": "^20.1.0",
"docs-soap": "^1.2.1",
"idea-toolbox": "^7.0.5",
"ionicons": "^7.2.2",
"maplibre-gl": "^4.1.0",
Expand Down
113 changes: 113 additions & 0 deletions front-end/src/app/common/htmlEditor.component.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import {
Component,
EventEmitter,
Input,
OnChanges,
OnInit,
Output,
SimpleChanges,
inject,
SecurityContext
} from '@angular/core';
import { DomSanitizer } from '@angular/platform-browser';
import { IonicModule } from '@ionic/angular';
import { AngularEditorConfig, AngularEditorModule } from '@kolkov/angular-editor';
import { docsSoap } from 'docs-soap';
import { HttpClientModule } from '@angular/common/http';

@Component({
standalone: true,
imports: [CommonModule, FormsModule, IonicModule, HttpClientModule, AngularEditorModule],
selector: 'app-html-editor',
template: `
@if(editMode){
<angular-editor
[(ngModel)]="text"
[config]="editorConfig"
(input)="contentChange.emit($event.target.innerHTML)"
(paste)="cleanHTMLCode()"
/>
}@else {
<div class="view" [innerHTML]="sanitizedHtml"></div>
}
`,
styles: [
`
div.view {
margin: var(--app-html-editor-margin, 0);
padding: var(--app-html-editor-padding, 20px);
background-color: var(--app-html-editor-background-color, var(--ion-color-white));
color: var(--app-html-editor-color, var(--ion-color-text));
box-shadow: var(--app-html-editor-box-shadow, none);
border: var(--app-html-editor-border-width, 1px) solid var(--ion-border-color);
border-radius: var(--app-html-editor-border-radius, 0);
}
`
]
})
export class HTMLEditorComponent implements OnInit, OnChanges {
/**
* Whether the parent page is in editMode or not (simplified).
*/
@Input() editMode = false;
/**
* The HTML content.
*/
@Input() content: string;
/**
* Trigger when the HTML content changes.
*/
@Output() contentChange = new EventEmitter<string>();

text: string;

editorConfig: AngularEditorConfig = {
editable: true,
spellcheck: false,
sanitize: true,
rawPaste: false,
minHeight: '300px',
toolbarHiddenButtons: [
[
'subscript',
'superscript',
'justifyLeft',
'justifyCenter',
'justifyRight',
'justifyFull',
'heading',
'fontName'
],
[
'fontSize',
'textColor',
'backgroundColor',
'customClasses',
'insertImage',
'insertVideo',
'insertHorizontalRule',
'toggleEditorMode'
]
]
};

sanitizedHtml: string;

_sanitizer = inject(DomSanitizer);

ngOnInit(): void {
this.text = this.content;
}
ngOnChanges(changes: SimpleChanges): void {
if (changes.editMode) this.sanitizedHtml = this._sanitizer.sanitize(SecurityContext.HTML, this.content);
}

cleanHTMLCode(): void {
setTimeout((): void => {
this.text = docsSoap(this.text);
this.contentChange.emit(this.text);
}, 100);
}
}
2 changes: 1 addition & 1 deletion front-end/src/app/common/map.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ const MAP_HTML_ELEMENT_ID = 'map';
const MAP_ENTITIES_LAYER_NAME = 'entities-layer';
const MAP_ENTITIES_SOURCE_NAME = 'entities-source';

const MAP_CENTER_LAT_LON: LngLatLike = [16, 45];
const MAP_CENTER_LAT_LON: LngLatLike = [-6, 37.4];
const MAP_DEFAULT_ZOOM = 14;

@Injectable({ providedIn: 'root' })
Expand Down
16 changes: 16 additions & 0 deletions front-end/src/app/common/media.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { Injectable } from '@angular/core';
import { IDEAApiService } from '@idea-ionic/common';

@Injectable({ providedIn: 'root' })
export class MediaService {
constructor(private api: IDEAApiService) {}

/**
* Upload a new image and get its URI.
*/
async uploadImage(file: File): Promise<string> {
const { url, id } = await this.api.postResource('media');
await fetch(url, { method: 'PUT', body: file, headers: { 'Content-Type': file.type } });
return id;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { CommonModule } from '@angular/common';
import { Component, EventEmitter, Input, Output } from '@angular/core';
import { IonicModule } from '@ionic/angular';
import { IDEATranslationsModule } from '@idea-ionic/common';

import { AppService } from 'src/app/app.service';

import { Communication } from '@models/communication.model';

@Component({
standalone: true,
imports: [CommonModule, IonicModule, IDEATranslationsModule],
selector: 'app-communication',
template: `
<ion-card [color]="color" *ngIf="!communication">
<ion-skeleton-text animated style="height: 180px"></ion-skeleton-text>
<ion-card-header>
<ion-card-subtitle>
<ion-skeleton-text animated style="width: 30%"></ion-skeleton-text>
</ion-card-subtitle>
<ion-card-title><ion-skeleton-text animated style="width: 60%"></ion-skeleton-text></ion-card-title>
<ion-card-subtitle><ion-skeleton-text animated style="width: 80%"></ion-skeleton-text></ion-card-subtitle>
</ion-card-header>
</ion-card>
<ion-card *ngIf="communication" [color]="color" [button]="button" (click)="select.emit()">
<ion-img *ngIf="communication.imageURI" [src]="app.getImageURLByURI(communication.imageURI)"></ion-img>
<ion-card-header>
<ion-card-subtitle>{{ communication.publishedAt | dateLocale : 'longDate' }}</ion-card-subtitle>
<ion-card-title>
<ion-text color="medium" style="font-weight: 600">
{{ communication.title }}
</ion-text>
</ion-card-title>
</ion-card-header>
<ion-card-content><ng-content></ng-content></ion-card-content>
</ion-card>
`,
styles: [
`
ion-card {
margin: 0 4px 16px 4px;
max-width: 600px;
}
ion-img {
object-fit: cover;
height: 180px;
border-bottom: 1px solid var(--ion-color-light);
}
ion-card-subtitle:first-of-type {
color: var(--ion-color-step-500);
}
ion-card-subtitle:nth-of-type(2) {
margin-top: 4px;
font-weight: 400;
}
`
]
})
export class CommunicationComponent {
/**
* The communication to show; if not set, shows a skeleton instead.
*/
@Input() communication: Communication;
/**
* The color for the component.
*/
@Input() color = 'white';
/**
* Whether the component should act like a button.
*/
@Input() button = false;
/**
* Trigger when selected.
*/
@Output() select = new EventEmitter<void>();

constructor(public app: AppService) {}
}
Loading

0 comments on commit 4d8bcd1

Please sign in to comment.