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

[IDOL, shoutouts] - edit shoutouts #578

Open
wants to merge 16 commits into
base: main
Choose a base branch
from
Open
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
26 changes: 26 additions & 0 deletions backend/src/API/shoutoutAPI.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,32 @@ export const hideShoutout = async (
await shoutoutsDao.updateShoutout({ ...shoutout, hidden: hide });
};

/**
* Edits a shoutout, ensuring the user has the necessary permissions or ownership.
* @throws {NotFoundError} If no shoutout with the provided uuid is found.
* @throws {PermissionError} If the user is neither the giver of the shoutout nor an admin or lead.
*/
export const editShoutout = async (
uuid: string,
newMessage: string,
user: IdolMember
): Promise<Shoutout> => {
const shoutout = await shoutoutsDao.getShoutout(uuid);
kevinmram marked this conversation as resolved.
Show resolved Hide resolved
if (!shoutout) {
throw new NotFoundError(`Shoutout with uuid: ${uuid} does not exist!`);
}
const isLeadOrAdmin = await PermissionsManager.isLeadOrAdmin(user);
if (!isLeadOrAdmin && shoutout.giver.email !== user.email) {
throw new PermissionError(
`You are not a lead or admin, so you can't edit a shoutout from a different user!`
);
}
return shoutoutsDao.updateShoutout({
...shoutout,
message: newMessage
});
};

/**
* Deletes a shoutout, ensuring the user has the necessary permissions or ownership.
* @throws {NotFoundError} If no shoutout with the provided uuid is found.
Expand Down
10 changes: 8 additions & 2 deletions backend/src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import * as winston from 'winston';
import * as expressWinston from 'express-winston';
import { app as adminApp, env } from './firebase';
import PermissionsManager from './utils/permissionsManager';
import { HandlerError } from './utils/errors';
import {
acceptIDOLChanges,
getIDOLChangesPR,
Expand All @@ -30,7 +29,8 @@ import {
getShoutouts,
giveShoutout,
hideShoutout,
deleteShoutout
deleteShoutout,
editShoutout
} from './API/shoutoutAPI';
import {
createCoffeeChat,
Expand Down Expand Up @@ -94,6 +94,7 @@ import { getWriteSignedURL, getReadSignedURL, deleteImage } from './API/imageAPI
import DPSubmissionRequestLogDao from './dao/DPSubmissionRequestLogDao';
import AdminsDao from './dao/AdminsDao';
import { sendMail } from './API/mailAPI';
import { HandlerError } from './utils/errors';

// Constants and configurations
const app = express();
Expand Down Expand Up @@ -284,6 +285,11 @@ loginCheckedPut('/shoutout', async (req, user) => {
return {};
});

loginCheckedPut('/shoutout/:uuid', async (req, user) => {
await editShoutout(req.params.uuid, req.body.message, user);
return {};
});

loginCheckedDelete('/shoutout/:uuid', async (req, user) => {
await deleteShoutout(req.params.uuid, user);
return {};
Expand Down
4 changes: 4 additions & 0 deletions frontend/src/API/ShoutoutsAPI.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,10 @@ export default class ShoutoutsAPI {
return APIWrapper.put(`${backendURL}/shoutout`, { uuid, hide }).then((res) => res.data);
}

public static updateShoutout(uuid: string, shoutout: Shoutout): Promise<ShoutoutResponseObj> {
return APIWrapper.put(`${backendURL}/shoutout/${uuid}`, shoutout).then((res) => res.data);
}

public static async deleteShoutout(uuid: string): Promise<void> {
await APIWrapper.delete(`${backendURL}/shoutout/${uuid}`);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,14 +27,31 @@
.shoutoutFrom {
color: gray;
display: inline-block;
padding-left: 1.5rem;
padding-left: 1rem;
padding-bottom: 1rem;
align-self: left;
}

.shoutoutDelete {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 0 !important;
margin-bottom: 0 !important;
}

.shoutoutActions {
display: flex;
justify-content: space-between;
align-items: center;
}

.editIcon {
color: #4183c4;
cursor: pointer;
margin-right: 10px !important;
transition: color 0.3s ease;
}

.editIcon:hover {
color: #1e70bf;
}
78 changes: 60 additions & 18 deletions frontend/src/components/Forms/ShoutoutsPage/ShoutoutCard.tsx
Original file line number Diff line number Diff line change
@@ -1,22 +1,32 @@
import React, { useState, useEffect } from 'react';
import { Card, Image, Loader } from 'semantic-ui-react';
import React, { useState, useEffect, Dispatch, SetStateAction } from 'react';
import { Button, Card, Image, Loader, Form, Icon, Modal, TextArea } from 'semantic-ui-react';
import ShoutoutsAPI from '../../../API/ShoutoutsAPI';
import ImagesAPI from '../../../API/ImagesAPI';
import ShoutoutDeleteModal from '../../Modals/ShoutoutDeleteModal';
import styles from './ShoutoutCard.module.css';
import ImagesAPI from '../../../API/ImagesAPI';

const ShoutoutCard = (props: {
interface ShoutoutCardProps {
shoutout: Shoutout;
setGivenShoutouts: React.Dispatch<React.SetStateAction<Shoutout[]>>;
}): JSX.Element => {
const { shoutout, setGivenShoutouts } = props;
setGivenShoutouts: Dispatch<SetStateAction<Shoutout[]>>;
}

const ShoutoutCard: React.FC<ShoutoutCardProps> = ({ shoutout, setGivenShoutouts }) => {
const [isEditing, setIsEditing] = useState(false);
const [editedMessage, setEditedMessage] = useState(shoutout.message);
const [image, setImage] = useState('');
const [isLoading, setIsLoading] = useState(false);

const fromString = shoutout.isAnon
? 'From: Anonymous'
: `From: ${shoutout.giver?.firstName || ''} ${shoutout.giver?.lastName || ''}`.trim();
const dateString = new Date(shoutout.timestamp).toDateString();
const handleEditShoutout = async () => {
const updatedShoutout = {
...shoutout,
message: editedMessage
};
await ShoutoutsAPI.updateShoutout(shoutout.uuid, updatedShoutout);
setGivenShoutouts((prevShoutouts) =>
prevShoutouts.map((s) => (s.uuid === shoutout.uuid ? { ...s, message: editedMessage } : s))
);
setIsEditing(false);
};

useEffect(() => {
if (shoutout.images && shoutout.images.length > 0) {
Expand All @@ -26,24 +36,34 @@ const ShoutoutCard = (props: {
setImage(url);
setIsLoading(false);
})
.catch(() => {
setIsLoading(false);
});
.catch(() => setIsLoading(false));
}
}, [shoutout.images]);

const fromString = shoutout.isAnon
? 'From: Anonymous'
: `From: ${shoutout.giver?.firstName || ''} ${shoutout.giver?.lastName || ''}`.trim();
const dateString = new Date(shoutout.timestamp).toDateString();

return (
<Card className={styles.shoutoutCardContainer}>
<Card.Group widths="equal" className={styles.shoutoutCardDetails}>
<Card.Content header={`To: ${shoutout.receiver}`} className={styles.shoutoutTo} />
<Card.Content className={styles.shoutoutDate} content={dateString} />
</Card.Group>
<Card.Group widths="equal" className={styles.shoutoutDelete}>
<div className={styles.shoutoutActions}>
<Card.Meta className={styles.shoutoutFrom} content={fromString} />
<ShoutoutDeleteModal uuid={shoutout.uuid} setGivenShoutouts={setGivenShoutouts} />
</Card.Group>
<div className={styles.actionIcons}>
<Icon
name="edit"
onClick={() => setIsEditing(true)}
className={styles.editIcon}
aria-label="Edit Shoutout"
/>
<ShoutoutDeleteModal uuid={shoutout.uuid} setGivenShoutouts={setGivenShoutouts} />
</div>
</div>
<Card.Content description={shoutout.message} />

{isLoading ? (
<Loader active inline />
) : (
Expand All @@ -55,6 +75,28 @@ const ShoutoutCard = (props: {
</Card.Content>
)
)}
<Modal open={isEditing} onClose={() => setIsEditing(false)}>
<Modal.Header>Edit Shoutout</Modal.Header>
<Modal.Content>
<Form>
<Form.Field>
<label>Message</label>
<TextArea
value={editedMessage}
onChange={(e, { value }) => setEditedMessage((value || '') as string)}
/>
</Form.Field>
</Form>
</Modal.Content>
<Modal.Actions>
<Button onClick={() => setIsEditing(false)} negative>
Cancel
</Button>
<Button onClick={handleEditShoutout} positive>
Save
</Button>
</Modal.Actions>
</Modal>
</Card>
);
};
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
.trashIcon {
position: absolute;
right: 1%;
margin-top: 2rem;
position: relative;
right: 10%;
}
Loading