From 9999e64e9b02feb2fd44dcf7225b82214b6d2565 Mon Sep 17 00:00:00 2001 From: apedrob Date: Mon, 4 Apr 2022 18:38:50 +0100 Subject: [PATCH] Add Appreciation Board --- back-end/deploy/main.ts | 1 + back-end/src/handlers/messages.ts | 116 ++++++++++++++++++++++ back-end/src/models/message.ts | 26 +++++ front-end/src/App.tsx | 10 +- front-end/src/pages/Appreciation.tsx | 140 +++++++++++++++++++++++++++ front-end/src/pages/Menu.tsx | 7 +- front-end/src/utils/data.tsx | 20 ++++ package-lock.json | 6 ++ 8 files changed, 324 insertions(+), 2 deletions(-) create mode 100644 back-end/src/handlers/messages.ts create mode 100644 back-end/src/models/message.ts create mode 100644 front-end/src/pages/Appreciation.tsx create mode 100644 package-lock.json diff --git a/back-end/deploy/main.ts b/back-end/deploy/main.ts index 82aaa7d..18226a6 100755 --- a/back-end/deploy/main.ts +++ b/back-end/deploy/main.ts @@ -30,6 +30,7 @@ const apiResources: ApiResourceController[] = [ const tables: { [tableName: string]: ApiTable } = { userProfiles: { PK: { name: 'userId', type: DDB.AttributeType.STRING } }, organizations: { PK: { name: 'organizationId', type: DDB.AttributeType.STRING } }, + messages: { PK: { name: 'messageId', type: DDB.AttributeType.STRING } }, speakers: { PK: { name: 'speakerId', type: DDB.AttributeType.STRING } }, venues: { PK: { name: 'venueId', type: DDB.AttributeType.STRING } }, sessions: { PK: { name: 'sessionId', type: DDB.AttributeType.STRING } }, diff --git a/back-end/src/handlers/messages.ts b/back-end/src/handlers/messages.ts new file mode 100644 index 0000000..bd72c5d --- /dev/null +++ b/back-end/src/handlers/messages.ts @@ -0,0 +1,116 @@ +/// +/// IMPORTS +/// + +import { DynamoDB, RCError, ResourceController, SES, EmailData, S3 } from 'idea-aws'; + +import { Message } from '../models/message'; +import { UserProfile } from '../models/userProfile'; + +/// +/// CONSTANTS, ENVIRONMENT VARIABLES, HANDLER +/// + +const PROJECT = process.env.PROJECT; + +const DDB_TABLES = { messages: process.env.DDB_TABLE_messages, profiles: process.env.DDB_TABLE_userProfiles }; + +const SES_CONFIG = { + sourceName: 'EGM', + source: process.env.SES_SOURCE_ADDRESS, + sourceArn: process.env.SES_IDENTITY_ARN, + region: process.env.SES_REGION +}; + +const S3_BUCKET_MEDIA = process.env.S3_BUCKET_MEDIA; +const S3_USERS_CV_FOLDER = process.env.S3_USERS_CV_FOLDER; + +const ddb = new DynamoDB(); +const ses = new SES(); +const s3 = new S3(); + +export const handler = (ev: any, _: any, cb: any) => new Messages(ev, cb).handleRequest(); + +/// +/// RESOURCE CONTROLLER +/// + +class Messages extends ResourceController { + message: Message; + + constructor(event: any, callback: any) { + super(event, callback, { resourceId: 'messageId' }); + } + + protected async checkAuthBeforeRequest(): Promise { + if (!this.resourceId) return; + + try { + this.message = new Message( + await ddb.get({ TableName: DDB_TABLES.messages, Key: { organizationId: this.resourceId } }) + ); + } catch (err) { + throw new RCError('Organization not found'); + } + } + + protected async getResource(): Promise { + return this.message; + } + + protected async putResource(): Promise { + const oldResource = new Message(this.message); + this.message.safeLoad(this.body, oldResource); + + return await this.putSafeResource(); + } + private async putSafeResource(opts: { noOverwrite?: boolean } = {}): Promise { + const errors = this.message.validate(); + if (errors.length) throw new RCError(`Invalid fields: ${errors.join(', ')}`); + + try { + const putParams: any = { TableName: DDB_TABLES.messages, Item: this.message }; + if (opts.noOverwrite) putParams.ConditionExpression = 'attribute_not_exists(messageId)'; + await ddb.put(putParams); + + return this.message; + } catch (err) { + throw new RCError('Operation failed'); + } + } + + protected async patchResource(): Promise { + switch (this.body.action) { + case 'SEND_USER_CONTACTS': + return ; //await this.likeMessage() + default: + throw new RCError('Unsupported action'); + } + } + + protected async deleteResource(): Promise { + if (!this.cognitoUser.isAdmin()) throw new RCError('Unauthorized'); + + try { + await ddb.delete({ TableName: DDB_TABLES.messages, Key: { messageId: this.resourceId } }); + } catch (err) { + throw new RCError('Delete failed'); + } + } + + protected async postResources(): Promise { + this.message = new Message(this.body); + this.message.messageId = await ddb.IUNID(PROJECT); + + return await this.putSafeResource({ noOverwrite: true }); + } + + protected async getResources(): Promise { + try { + return (await ddb.scan({ TableName: DDB_TABLES.messages })) + .map((x: Message) => new Message(x)); + } catch (err) { + throw new RCError('Operation failed'); + } + } +} diff --git a/back-end/src/models/message.ts b/back-end/src/models/message.ts new file mode 100644 index 0000000..1a7e172 --- /dev/null +++ b/back-end/src/models/message.ts @@ -0,0 +1,26 @@ +import { isEmpty, Resource } from 'idea-toolbox'; + +export class Message extends Resource { + messageId: string; + senderId: string; + senderName: string; + text: string; + + load(x: any): void { + super.load(x); + this.messageId = this.clean(x.messageId, String); + this.senderId = this.clean(x.senderId, String); + this.senderName = this.clean(x.senderName, String); + this.text = this.clean(x.text, String); + } + safeLoad(newData: any, safeData: any): void { + super.safeLoad(newData, safeData); + this.messageId = safeData.messageId; + } + validate(): string[] { + const e = super.validate(); + if (isEmpty(this.text)) e.push('message'); + if (this.senderName && isEmpty(this.senderName)) e.push('sender'); + return e; + } +} diff --git a/front-end/src/App.tsx b/front-end/src/App.tsx index a1dbb5f..b59973d 100644 --- a/front-end/src/App.tsx +++ b/front-end/src/App.tsx @@ -14,7 +14,7 @@ import { setupIonicReact } from '@ionic/react'; import { IonReactRouter } from '@ionic/react-router'; -import { calendar, map, menu, people } from 'ionicons/icons'; +import { calendar, map, menu, people, clipboard } from 'ionicons/icons'; import '@ionic/react/css/core.css'; import '@ionic/react/css/normalize.css'; @@ -40,6 +40,7 @@ import SpeakerPage from './pages/Speaker'; import OrganizationsPage from './pages/Organizations'; import OrganizationPage from './pages/Organization'; import ManageEntityPage from './pages/ManageEntity'; +import AppreciationPage from './pages/Appreciation'; import { isMobileMode } from './utils'; @@ -113,6 +114,9 @@ const App: React.FC = () => ( + + + ( {isMobileMode() ? : ''} You + + {isMobileMode() ? : ''} + Appreciation + {isMobileMode() ? : ''} Menu diff --git a/front-end/src/pages/Appreciation.tsx b/front-end/src/pages/Appreciation.tsx new file mode 100644 index 0000000..24b044b --- /dev/null +++ b/front-end/src/pages/Appreciation.tsx @@ -0,0 +1,140 @@ +import { Auth } from '@aws-amplify/auth'; +import { + IonContent, + IonHeader, + IonIcon, + IonItem, + IonItemDivider, + IonLabel, + IonList, + IonPage, + IonTitle, + IonToolbar, + useIonAlert, + IonNote, + IonTextarea, + IonButton, + IonText, + IonItemGroup, +} from '@ionic/react'; +import { + close, + heartOutline, + send, +} from 'ionicons/icons'; +import { useEffect, useState } from 'react'; + +import { UserProfile } from 'models/userProfile'; +import { Message } from 'models/message'; + +import { isMobileMode } from '../utils'; +import { + isUserAdmin, getUserProfile, getMessages, + sendMessage, + deleteMessage, +} from '../utils/data'; + +// TODO placeholder +const allMessages = [{ + messageId: "1234-uuid", + senderId: "3456-uuid", + senderName: "Jose Gonzalez", + text: "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam viverra diam quis odio hendrerit molestie. Nam iaculis nunc eget urna tincidunt, sit amet fringilla ex aliquet. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam malesuada molestie condimentum. Duis placerat enim vel ipsum gravida pellentesque sit amet ut sapien.", +} as Message] + +const AppreciationPage: React.FC = () => { + const [showAlert] = useIonAlert(); + + const [userIsAdmin, setUserIsAdmin] = useState(false); + const [userProfile, setUserProfile] = useState(); + + const [text, setText] = useState(); + const [messages, setMessages] = useState(); + + + console.log(userProfile); + useEffect(() => { + const loadData = async () => { + const userProfile = await getUserProfile(); + setUserProfile(userProfile); + + setUserIsAdmin(await isUserAdmin()); + + + setMessages(allMessages); // setMessages(await getMessages()); + }; + loadData(); + }, []); + + + const handleSendMessage = async () => { + const message = { + senderId: userProfile?.userId, + senderName: userProfile?.getName(), + text: text, + } + + await sendMessage(message as Message); + setText(""); + } + + const handleDeleteMessage = async (message: Message) => { + await deleteMessage(message); + } + + return ( + + + {isMobileMode() ? ( + + Appreciation + + ) : ( + '' + )} + + + + {messages?.map(message => ( + + + {message.senderName} + + + + {0} + + + {userIsAdmin && handleDeleteMessage(message)}> + + Delete + } + + + + {message.text} + + + ))} + + + Write a Message + + + + + setText(e.detail.value!)}> + handleSendMessage()} > + Send + + + + + + + + + ); +}; + +export default AppreciationPage; diff --git a/front-end/src/pages/Menu.tsx b/front-end/src/pages/Menu.tsx index 514c715..077e797 100644 --- a/front-end/src/pages/Menu.tsx +++ b/front-end/src/pages/Menu.tsx @@ -25,7 +25,8 @@ import { people, peopleOutline, person, - refresh + refresh, + clipboard } from 'ionicons/icons'; import { useEffect, useState } from 'react'; @@ -97,6 +98,10 @@ const MenuPage: React.FC = () => { Speakers + + + Appreciation + Profile diff --git a/front-end/src/utils/data.tsx b/front-end/src/utils/data.tsx index d8458e2..df0b4d7 100644 --- a/front-end/src/utils/data.tsx +++ b/front-end/src/utils/data.tsx @@ -7,6 +7,7 @@ import { Organization } from 'models/organization'; import { Venue } from 'models/venue'; import { Speaker } from 'models/speaker'; import { Session } from 'models/session'; +import { Message } from 'models/message'; import { getEnv } from '../environment'; @@ -172,6 +173,25 @@ export const sendUserContactsToOrganization = async ( }); }; + +// +// MESSAGES +// + +export const getMessages = async (): Promise => { + return (await apiRequest('GET', 'messages')).map((x: Message) => new Message(x)); +}; + +export const sendMessage = async (message: Message): Promise => { + if (message.messageId) + return await apiRequest('PUT', ['messages', message.messageId], message); + else return await apiRequest('POST', 'messages', message); +}; + +export const deleteMessage = async (message: Message): Promise => { + return await apiRequest('DELETE', ['messages', message.messageId]); +}; + // // IMAGES // diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..17704bd --- /dev/null +++ b/package-lock.json @@ -0,0 +1,6 @@ +{ + "name": "egm-app", + "lockfileVersion": 2, + "requires": true, + "packages": {} +}