Skip to content

Commit

Permalink
Add Appreciation Board
Browse files Browse the repository at this point in the history
  • Loading branch information
apedrob committed Apr 4, 2022
1 parent e35e535 commit 1841590
Show file tree
Hide file tree
Showing 8 changed files with 332 additions and 2 deletions.
1 change: 1 addition & 0 deletions back-end/deploy/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 } },
Expand Down
124 changes: 124 additions & 0 deletions back-end/src/handlers/messages.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
///
/// 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 EMAIL_CONTENTS = {
subject: '[EGM] Contact request',
textHeader:
"Hi,\nthis is an automatic email from the EGM app.\n\nI'd like to get in touch with your organization; therefore, here is my contact information:\n\n",
textAttachment: '\n\nYou can find attached my CV.\n',
textFooter: '\n\nBest regards,\n'
};

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<void> {
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<Message> {
return this.message;
}

protected async putResource(): Promise<Message> {
const oldResource = new Message(this.message);
this.message.safeLoad(this.body, oldResource);

return await this.putSafeResource();
}
private async putSafeResource(opts: { noOverwrite?: boolean } = {}): Promise<Message> {
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<void> {
switch (this.body.action) {
case 'SEND_USER_CONTACTS':
return ; //await this.likeMessage()
default:
throw new RCError('Unsupported action');
}
}

protected async deleteResource(): Promise<void> {
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<Message> {
this.message = new Message(this.body);
this.message.messageId = await ddb.IUNID(PROJECT);

return await this.putSafeResource({ noOverwrite: true });
}

protected async getResources(): Promise<Message[]> {
try {
return (await ddb.scan({ TableName: DDB_TABLES.messages }))
.map((x: Message) => new Message(x));
} catch (err) {
throw new RCError('Operation failed');
}
}
}
26 changes: 26 additions & 0 deletions back-end/src/models/message.ts
Original file line number Diff line number Diff line change
@@ -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;
}
}
10 changes: 9 additions & 1 deletion front-end/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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';

Expand Down Expand Up @@ -113,6 +114,9 @@ const App: React.FC = () => (
<Route exact path="/">
<Redirect to="/sessions" />
</Route>
<Route path="/appreciation">
<AppreciationPage />
</Route>
</IonRouterOutlet>
<IonTabBar
color="ideaToolbar"
Expand All @@ -132,6 +136,10 @@ const App: React.FC = () => (
{isMobileMode() ? <IonIcon icon={people} /> : ''}
<IonLabel>You</IonLabel>
</IonTabButton>
<IonTabButton tab="appreciation" href="/appreciation">
{isMobileMode() ? <IonIcon icon={clipboard} /> : ''}
<IonLabel>Appreciation</IonLabel>
</IonTabButton>
<IonTabButton tab="menu" href="/menu">
{isMobileMode() ? <IonIcon icon={menu} /> : ''}
<IonLabel>Menu</IonLabel>
Expand Down
140 changes: 140 additions & 0 deletions front-end/src/pages/Appreciation.tsx
Original file line number Diff line number Diff line change
@@ -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<UserProfile>();

const [text, setText] = useState<string>();
const [messages, setMessages] = useState<Message[]>();


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 (
<IonPage>
<IonHeader>
{isMobileMode() ? (
<IonToolbar color="ideaToolbar">
<IonTitle>Appreciation</IonTitle>
</IonToolbar>
) : (
''
)}
</IonHeader>
<IonContent>
<IonList style={{ maxWidth: isMobileMode() ? "100%" : "50%", margin: '0 auto' }}>
{messages?.map(message => (
<IonItemGroup>
<IonItem color="white">
<IonNote slot="start">{message.senderName}</IonNote>

<IonButton fill="solid" slot="end" color="white">
<IonIcon icon={heartOutline} />
<IonNote style={{ marginLeft: '4px' }} slot="end">{0}</IonNote>
</IonButton>

{userIsAdmin && <IonButton fill="solid" slot="end" color="white" onClick={() => handleDeleteMessage(message)}>
<IonIcon icon={close} />
<IonNote style={{ marginLeft: '4px' }} slot="end">Delete</IonNote>
</IonButton>}

</IonItem>
<IonItem color="white" >
<IonText style={{ paddingBottom: '12px' }}>{message.text}</IonText>
</IonItem>
</IonItemGroup>
))}

<IonItemDivider>
<IonLabel>Write a Message</IonLabel>
</IonItemDivider>

<IonItemGroup color="white">
<IonItem color="white" >
<IonTextarea value={text} onIonChange={e => setText(e.detail.value!)}></IonTextarea>
<IonButton size="default" style={{ alignSelf: 'center' }} onClick={() => handleSendMessage()} >
Send
<IonIcon icon={send} slot="end"></IonIcon>
</IonButton>
</IonItem>
</IonItemGroup>

</IonList>
</IonContent>
</IonPage>
);
};

export default AppreciationPage;
7 changes: 6 additions & 1 deletion front-end/src/pages/Menu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@ import {
people,
peopleOutline,
person,
refresh
refresh,
clipboard
} from 'ionicons/icons';
import { useEffect, useState } from 'react';

Expand Down Expand Up @@ -97,6 +98,10 @@ const MenuPage: React.FC = () => {
<IonIcon icon={people} slot="start"></IonIcon>
<IonLabel>Speakers</IonLabel>
</IonItem>
<IonItem button color="white" routerLink="/appreciation">
<IonIcon icon={clipboard} slot="start"></IonIcon>
<IonLabel>Appreciation</IonLabel>
</IonItem>
<IonItem button color="white" routerLink="/user">
<IonIcon icon={person} slot="start"></IonIcon>
<IonLabel>Profile</IonLabel>
Expand Down
20 changes: 20 additions & 0 deletions front-end/src/utils/data.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -172,6 +173,25 @@ export const sendUserContactsToOrganization = async (
});
};


//
// MESSAGES
//

export const getMessages = async (): Promise<Message[]> => {
return (await apiRequest('GET', 'messages')).map((x: Message) => new Message(x));
};

export const sendMessage = async (message: Message): Promise<Message> => {
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<void> => {
return await apiRequest('DELETE', ['messages', message.messageId]);
};

//
// IMAGES
//
Expand Down
Loading

0 comments on commit 1841590

Please sign in to comment.