Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[DRAFT] Add Appreciation Board #49

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
116 changes: 116 additions & 0 deletions back-end/src/handlers/messages.ts
Original file line number Diff line number Diff line change
@@ -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<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
6 changes: 6 additions & 0 deletions package-lock.json

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