Skip to content

Commit

Permalink
Complete Notification Page Functionality (Frontend) (#39)
Browse files Browse the repository at this point in the history
* Add notification drawer and grouping

* Add account pending component

* Added callbacks to approval

* Add finishing touches

* Touch up styles on drawer

* Added toasts

* Prettier formatting

---------

Co-authored-by: Sean Fong <[email protected]>
Co-authored-by: ThatMegamind <[email protected]>
  • Loading branch information
3 people authored Feb 20, 2024
1 parent e4159e8 commit beaf4f5
Show file tree
Hide file tree
Showing 7 changed files with 567 additions and 102 deletions.
10 changes: 3 additions & 7 deletions src/App.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import { CookiesProvider } from 'react-cookie';
import Login from './components/Authentication/Login';
import Logout from './components/Authentication/Logout';
import SignUp from './components/Authentication/SignUp';
import Notifications from './pages/Notifications/Notifications';
import ForgotPassword from './components/Authentication/ForgotPassword';
import EmailAction from './components/Authentication/EmailAction';
import AUTH_ROLES from './utils/auth_config';
Expand All @@ -14,6 +13,7 @@ import Catalog from './pages/Catalog/Catalog';
import PublishedSchedule from './pages/PublishedSchedule/PublishedSchedule';
import Playground from './pages/Playground/Playground';
import Planner from './pages/Planner/Planner';
import NotificationSandbox from './pages/NotificationSandbox/NotificationSandbox';

const { ADMIN_ROLE, USER_ROLE } = AUTH_ROLES.AUTH_ROLES;

Expand All @@ -40,13 +40,9 @@ const App = () => {
<Route exact path="/emailAction" element={<EmailAction redirectPath="/" />} />
<Route
exact
path="/notifications"
path="/notification-sandbox"
element={
<ProtectedRoute
Component={Notifications}
redirectPath="/login"
roles={[ADMIN_ROLE, USER_ROLE]}
/>
<NotificationSandbox />
}
/>
<Route
Expand Down
247 changes: 247 additions & 0 deletions src/components/Notifications/AccountNotification.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,247 @@
import React, { useState } from 'react';
import PropTypes from 'prop-types';
import { AccountNotificationBlock } from './NotificationElement';
import {
Accordion,
AccordionItem,
AccordionButton,
AccordionIcon,
AccordionPanel,
ButtonGroup,
Button,
Container,
Grid,
GridItem,
Text,
useToast,
} from '@chakra-ui/react';

const AccountNotification = ({ notificationBlock, today, removeEntry }) => {
const toast = useToast();
const [accounts, setAccounts] = useState(notificationBlock.getNotificationData().accounts);
const [disableChildrenButtons, setDisableChildrenButtons] = useState(false);

const blockDate = notificationBlock.getDate();
const diffTime = today - blockDate;
const diffDays = Math.floor(diffTime / (1000 * 60 * 60 * 24));

const acceptAll = async accounts => {
await Promise.all(
accounts.map(async account => {
await account.approveCallback();
}),
);

toast({
title: `Approved ${accounts?.length} accounts.`,
status: 'success',
duration: 9000,
isClosable: true,
});

removeEntry(notificationBlock.key);
};

const declineAll = async accounts => {
await Promise.all(
accounts.map(async account => {
await account.declineCallback();
}),
);

toast({
title: `Declined ${accounts?.length} accounts.`,
status: 'info',
duration: 9000,
isClosable: true,
});
removeEntry(notificationBlock.key);
};

return (
<Container p="0" m="0" maxWidth="none" display="flex" flexDirection="column">
<div>
{accounts?.length === 1 ? (
<>
{/* No accordion for 1 account notification */}
<Text pb="4">
<Text color="blue.600" as="b" display="inline">
{accounts?.[0]?.email}{' '}
</Text>
<Text as="span" display="inline" whiteSpace="wrap">
is requesting account approval...
</Text>
</Text>
<AccountButtonGroup
acceptText="Accept"
declineText="Decline"
acceptCallback={() => {
acceptAll(accounts);
}}
declineCallback={() => {
declineAll(accounts);
}}
pl="3.25rem"
/>
</>
) : (
<>
{/* Accordion for >1 account notification in block */}
<Text pb="4">
<Text as="b" color="blue.600">
{accounts?.[0]?.email} and {accounts?.length - 1} other
{accounts?.length - 1 > 1 && 's'}{' '}
</Text>
<Text as="span" display="inline" whiteSpace="wrap">
{accounts?.length - 1 > 1 ? 'are' : 'is'} requesting account approval...
</Text>
</Text>
<Accordion allowToggle>
<AccordionItem border="none">
<Container display="flex" gap="6" alignItems="center" w="fit-content" m="0" p="0">
<AccordionButton
width="fit-content"
p="1"
borderRadius="100"
backgroundColor="gray.200"
>
<AccordionIcon />
</AccordionButton>
<AccountButtonGroup
acceptText="Accept All"
declineText="Decline All"
acceptCallback={() => {
setDisableChildrenButtons(true);
acceptAll(accounts);
}}
declineCallback={() => {
setDisableChildrenButtons(true);
declineAll(accounts);
}}
/>
</Container>
<AccordionPanel whiteSpace="wrap" paddingLeft="3.25rem">
<Grid templateColumns="1fr auto" gap={6}>
{accounts?.map(({ id, email, approveCallback, declineCallback }) => (
<React.Fragment key={id}>
<GridItem>
<Text color="blue.600" decoration="underline">
{email}
</Text>
</GridItem>
<GridItem>
<AccountButtonGroup
acceptText="Accept"
declineText="Decline"
acceptCallback={async () => {
await approveCallback();
toast({
title: `Approved ${email}.`,
status: 'success',
duration: 9000,
isClosable: true,
});
setAccounts(accounts =>
accounts.filter(account => account.id !== id),
);
}}
declineCallback={async () => {
await declineCallback();
toast({
title: `Declined ${email}.`,
status: 'info',
duration: 9000,
isClosable: true,
});
setAccounts(accounts =>
accounts.filter(account => account.id !== id),
);
}}
disableChildrenButtons={disableChildrenButtons}
/>
</GridItem>
</React.Fragment>
))}
</Grid>
</AccordionPanel>
</AccordionItem>
</Accordion>
</>
)}
</div>
<Text alignSelf="flex-end" fontSize="xs" title={blockDate.toLocaleString()}>
{diffDays > 0 ? (
<>
{diffDays} day{diffDays !== 1 && 's'} ago
</>
) : (
<>Today</>
)}
</Text>
</Container>
);
};
AccountNotification.propTypes = {
notificationBlock: PropTypes.instanceOf(AccountNotificationBlock),
today: PropTypes.instanceOf(Date),
removeEntry: PropTypes.func,
};

const AccountButtonGroup = ({
acceptText,
acceptCallback,
declineText,
declineCallback,
disableChildrenButtons,
...chakraProps
}) => {
const [acceptState, setAcceptState] = useState(undefined);
const [declineState, setDeclineState] = useState(undefined);

return (
<ButtonGroup gap="2" {...chakraProps}>
<Button
onClick={() => {
setAcceptState('loading');
setDeclineState('disabled');
acceptCallback();
}}
backgroundColor="blue.500"
color="white"
fontSize="sm"
h="6"
fontWeight="normal"
isLoading={acceptState === 'loading'}
isDisabled={acceptState === 'disabled' || disableChildrenButtons === true}
>
{acceptText}
</Button>
<Button
onClick={() => {
setDeclineState('loading');
setAcceptState('disabled');
declineCallback();
}}
backgroundColor="gray.200"
color="black"
fontSize="sm"
h="6"
fontWeight="normal"
isLoading={declineState === 'loading'}
isDisabled={declineState === 'disabled' || disableChildrenButtons === true}
>
{declineText}
</Button>
</ButtonGroup>
);
};
AccountButtonGroup.propTypes = {
acceptText: PropTypes.string,
acceptCallback: PropTypes.func,
declineText: PropTypes.string,
declineCallback: PropTypes.func,
disableChildrenButtons: PropTypes.bool,
chakraProps: PropTypes.any,
};

export default AccountNotification;
41 changes: 41 additions & 0 deletions src/components/Notifications/EventNotification.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import PropTypes from 'prop-types';
import { EventNotificationBlock } from './NotificationElement';
import { Container, Text } from '@chakra-ui/react';

const EventNotification = ({ notificationBlock, today }) => {
const { title, status } = notificationBlock.getNotificationData();

const blockDate = notificationBlock.getDate();
const diffTime = today - blockDate;
const diffDays = Math.floor(diffTime / (1000 * 60 * 60 * 24));

return (
<Container p="0" m="0" maxWidth="none" display="flex" flexDirection="column">
<Text pb="2">
<Text color="blue.600" as="b" display="inline">
Event: {title}
</Text>
<Text as="span" display="inline" whiteSpace="wrap">
{status === 'added' && ' was added to the schedule.'}
{status === 'confirmed' && ' was changed from tentative to confirmed.'}
</Text>
</Text>
<Text alignSelf="flex-end" fontSize="xs" title={blockDate.toLocaleString()}>
{diffDays > 0 ? (
<>
{diffDays} day{diffDays !== 1 && 's'} ago
</>
) : (
<>Today</>
)}
</Text>
</Container>
);
};

EventNotification.propTypes = {
notificationBlock: PropTypes.instanceOf(EventNotificationBlock),
today: PropTypes.instanceOf(Date),
};

export default EventNotification;
59 changes: 59 additions & 0 deletions src/components/Notifications/NotificationElement.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
class NotificationBlock {
constructor(date, type, key) {
this.date = date; // Date
this.type = type; // 'account' | 'event'
this.key = key;
}

getDate() {
return this.date;
}

getType() {
return this.type;
}

getKey() {
return this.key;
}

getNotificationData() {
// Abstract method must be implemented by sub-class
// Returns data to display for notification; format of data depends on
// the type of notification block (account vs event)
}
}

class AccountNotificationBlock extends NotificationBlock {
constructor(date, type, key) {
super(date, type, key);
this.pendingAccounts = [];
}

addPendingAccount(id, email, approveCallback, declineCallback) {
this.pendingAccounts.push({
id: id,
email: email,
approveCallback: approveCallback,
declineCallback: declineCallback,
});
}

getNotificationData() {
return { accounts: this.pendingAccounts };
}
}

class EventNotificationBlock extends NotificationBlock {
constructor(date, type, key, title, status) {
super(date, type, key);
this.title = title;
this.status = status; // 'added' | 'confirmed'
}

getNotificationData() {
return { title: this.title, status: this.status };
}
}

export { AccountNotificationBlock, EventNotificationBlock };
Loading

0 comments on commit beaf4f5

Please sign in to comment.