Skip to content

Instantly share code, notes, and snippets.

@cl0ckwork
Last active September 27, 2023 03:05
Show Gist options
  • Save cl0ckwork/d8494562de13f36193388c9fe3534167 to your computer and use it in GitHub Desktop.
Save cl0ckwork/d8494562de13f36193388c9fe3534167 to your computer and use it in GitHub Desktop.
Post Feed Refactor

Context

Imagine you are working for a "social" company that has a layout similar to facebook groups, with the following features listed below.

  • a user can join a group
  • a user must belong to an entity (ie: company)
  • a user can create posts in the group that are of different types (discussion or sales)
  • a user can view these posts from within the context of a group wall or feed, or within the context of their company or personal page
  • a user can edit a post once created.

Homework

Considering the features, please have a look at the PostFeed.tsx file which is an implementation of the component that wraps and renders the "feed" of the group. Please answer the below 2 questions in whatever format you'd like - preferably something that can display snippets (like markdown or a .tsx file).

  1. Pick at least 2 implementations you agree with, include a snippet and explain why you like and what you think the benefits/trade-offs are (this could be as simple as a hook implementation/use that you like).
  2. Pick 3 implementations you would refactor. Of the 3 you would refactor, please write some psuedo code and explain or demonstrate what you would change, and why. (This could be as complex or as simple as youd like)

Example (simple) - Please do not use this in your response.

const { sendMessage } = useSnackbar();

I like the above hook absraction for a snackbar, it provides a very simple interface for interacting with a, presumably, global implementation. As an engineer I can just grab it and go for my messaging use case. It also provides me a single location for updating a snackbar implementation.

There are no wrong/right answers - please keeop in mind this is how im getting insight into your thought process as an engineer

import React, { useState } from 'react';
import { useThemeStyles } from 'components/styles';
import { Post } from 'resources/domain/entity/IItem';
import useLang from 'components/hooks/useLang';
import { Box, Grid, makeStyles, Typography } from '@material-ui/core'
import { DropDown, MenuItem } from './CommonDropDown';
import { yupResolver } from '@hookform/resolvers/yup';
import InputCommentWrapper from 'components/utilities/InputCommentWrapper';
import { FormProvider, useForm } from 'react-hook-form';
import {
IItemInitialValues,
ItemCreateButtonState,
ItemValidationSchema,
} from 'components/views/GroupFeed';
import { getTextWithEllipsis } from 'util/getTextWithEllipsis';
import { CommonButton } from './CommonButton';
import { ChatBubbleOutline, LocalOffer } from '@material-ui/icons';
import { useSelector } from 'react-redux';
import { RootState } from 'state_management/reducers';
import { IItemDetailProps } from 'resources/domain/entity/IItem';
import {
ISaleItemDetailProps,
PostType,
} from 'resources/domain/entity/ISaleItem';
import userImage from 'assets/images/user-avatar.png';
import { useSnackbar } from 'components/notification/SnackbarNotification';
import { useMutation } from 'services/useMutation';
import { useSetUserInterestOnPusher } from 'services/notifications/pusher';
import PostFeed, { usePostCacheClearing } from '../views/GroupFeed/PostFeed';
import { GroupId } from 'src/services/groupService/models/groupModel';
import { PostTypeFilter } from 'components/views/GroupFeed/PostFeed';
import { RuleId } from 'src/services/rulesService/models/rules';
const CreateEngagementPost = React.lazy(
() => import('../views/CreateEngagementPost')
);
const CreateSalePost = React.lazy(() => import('../views/CreateSalePost'));
/**
* Styles logic for this component.
*/
const useStyles = makeStyles({
createPostCard: {
backgroundColor: '#fff',
borderRadius: '10px',
boxShadow: '0px 3px 6px #0000000D',
marginBottom: '15px',
paddingBottom: '10px',
},
createPostCardTop: {
borderBottom: '1px solid #707070',
padding: '15px',
'& h2': {
color: '#000',
fontSize: '22px',
fontWeight: 400,
height: '100%',
display: 'flex',
alignItems: 'center',
},
'& p': {
color: '#000',
fontSize: '22px',
fontWeight: 400,
'& a': {
color: '#000',
},
},
},
createPostCardMid: {
margin: '15px',
display: 'flex',
alignItems: 'center',
borderBottom: '1px solid #707070',
paddingBottom: '10px',
'& .MuiFormControl-root': {
width: '100%',
},
},
circleImage: {
'& img': {
borderRadius: '100%',
width: '50px',
height: '50px',
minWidth: '50px',
objectFit: 'cover',
objectPosition: 'center',
},
},
createPostCardBottom: {
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
padding: '15px',
'& span': {
color: '#707070',
display: 'flex',
alignItems: 'center',
marginRight: '5px',
cursor: 'pointer',
'& svg': {
width: '18px',
marginRight: '5px',
minWidth: '18px',
marginTop: '-3px',
},
},
},
});
/**
* This function handles all the posts in Group Feed view.
*/
const CommonGroupItemsList = ({
postType,
companyId,
groupId,
ruleId,
}: {
postType?: PostTypeFilter;
companyId?: string;
groupId: GroupId;
ruleId?: RuleId;
}): JSX.Element => {
const styleClasses = useStyles();
const commonStyleClasses = useThemeStyles();
const { f: translation } = useLang();
const { sendMessage } = useSnackbar();
const [loading, setLoading] = useState<boolean>();
const postCacheClearing = usePostCacheClearing();
const userInfo = useSelector((states: RootState) => states.authReducer.user);
const [showCreatePostModal, setShowCreatePostModal] = useState(false);
const [showCreateSalePostModal, setShowCreateSalePostModal] = useState(false);
const [item, setItem] = useState<Post>();
const { mutateAsync: updateSaleMutateAsync } = useMutation({
mutationKey: 'saleItemsAndOffers.updateSaleItem',
});
const { mutateAsync: createSaleMutateAsync } = useMutation({
mutationKey: 'saleItemsAndOffers.createSaleItem',
});
const { mutateAsync: createDiscussionMutateAsync } = useMutation({
mutationKey: 'items.createItemForGroup',
});
const { mutateAsync: updateDiscussionMutateAsync } = useMutation({
mutationKey: 'items.updateItemById',
});
// eventEmitter.subscribe(EventType.REFETCH_ITEM, 'refetch', () =>
// postsResponse.refetch()
// );
const ItemDropdown = [
{ id: 1, title: 'Discussion', icon: <ChatBubbleOutline /> },
{ id: 2, title: 'Sale', icon: <LocalOffer /> },
];
const { updateUserInterests } = useSetUserInterestOnPusher();
/**
* This function handles create post in a modal.
*/
const handleCreatePostModal = (id: string, value: boolean) => {
if (id === 'Discussion') setShowCreatePostModal(value);
if (id === 'Sale') setShowCreateSalePostModal(value);
setItem(undefined);
};
/**
* Setting Initial values for the Create Post
*/
const initialValues = {
title: '',
};
const methods = useForm({
resolver: yupResolver(ItemValidationSchema(translation)),
});
const {
handleSubmit,
reset,
watch,
formState: { errors },
} = methods;
/**
* This function creates the post.
*/
const onSubmit = async (
createItemPayload:
| {
postType: 'Discussion';
itemsData: IItemDetailProps;
isUpdate: boolean;
}
| {
postType: 'Sale';
itemsSaleData: ISaleItemDetailProps;
isUpdate: boolean;
}
) => {
if (!groupId) {
sendMessage({
message: translation('app.item.create.group.undefined').replace(
'postType',
createItemPayload.postType.toLowerCase()
),
});
return;
}
switch (createItemPayload.postType) {
case 'Discussion':
return (async () => {
const res = !createItemPayload.isUpdate
? await createDiscussionMutateAsync({
groupId,
data: createItemPayload.itemsData,
})
: item &&
(await updateDiscussionMutateAsync({
params: { groupId, itemId: item.id },
data: createItemPayload.itemsData,
}));
if ([res?.data?.code].includes(200)) {
sendMessage({
message: translation('app.item.message.create.success'),
severity: 'success',
});
reset({ ...initialValues });
postCacheClearing({
groupId,
// @ts-expect-error - these types are a mess
userId: userInfo.id,
// todo: find a better way to get this
// @ts-expect-error - companyId should not be optional on a user
companyId: userInfo.company.id,
});
// postsResponse.refetch();
handleCreatePostModal('Discussion', false);
updateUserInterests();
}
})();
case 'Sale':
return (async () => {
const res = !createItemPayload.isUpdate
? await createSaleMutateAsync({
groupId,
data: createItemPayload.itemsSaleData,
})
: item?.sale?.id
? await updateSaleMutateAsync({
groupId,
saleId: item?.sale?.id,
data: createItemPayload.itemsSaleData,
})
: undefined;
if ([res?.data?.code].includes(200)) {
sendMessage({
message: createItemPayload.isUpdate
? translation('app.item.message.update.success')
: translation('app.item.message.create.success'),
severity: 'success',
});
reset({ ...initialValues });
postCacheClearing({
groupId,
// @ts-expect-error - these types are a mess
userId: userInfo.id,
// todo: find a better way to get this
// @ts-expect-error - companyId should not be optional on a user
companyId: userInfo.company.id,
});
handleCreatePostModal('Sale', false);
updateUserInterests();
}
})();
default:
return ((_payload: never) => null)(createItemPayload);
}
};
/**
* The function gets the data from React hook form and creates discussion items in database
* If an item images are available then the function will upload it on S3 bucket and return a URL.
* It then appends the URL with the data and stores it in the database
*
* @param data
*/
const onCreateItem = async (data: IItemInitialValues) => {
setLoading(true);
const itemsData = {
title: data.title,
postType: PostType.DISCUSSION,
description: '',
commentsAllowed: true,
};
await onSubmit({ postType: 'Discussion', itemsData, isUpdate: false });
setLoading(false);
};
return (
<div>
{!companyId ? (
<Box className={styleClasses.createPostCard}>
<Box className={styleClasses.createPostCardTop}>
<Grid container>
<Grid xs={6}>
<Typography variant="h2">
{translation('app.item.create')}
</Typography>
</Grid>
<Grid xs={6}>
<Box className={commonStyleClasses.textRight}>
<DropDown
label={translation('app.feed.item.type.label')}
items={ItemDropdown}
onItemClick={(item: MenuItem) =>
handleCreatePostModal(item?.title as string, true)
}
/>
</Box>
</Grid>
</Grid>
</Box>
<FormProvider {...methods}>
<Box className={styleClasses.createPostCardMid}>
<Box className={styleClasses.circleImage}>
<img
src={userInfo.avatar ?? userImage}
alt={getTextWithEllipsis({ text: userInfo.name, limit: 10 })}
/>
</Box>
<InputCommentWrapper
name="title"
placeholder={translation('app.item.create.placeholder')}
type="text"
errorobj={errors as { [key: string]: { message: string } }}
multiline
minRows={1}
disabled={loading}
/>
<Typography align="right" className={commonStyleClasses.ml20}>
<CommonButton
size="medium"
label="Post"
onClick={handleSubmit(onCreateItem)}
loading={loading}
disabled={loading}
/>
</Typography>
</Box>
</FormProvider>
<Box className={styleClasses.createPostCardBottom}>
{ItemCreateButtonState?.map((info, index) => (
<span
key={index}
onClick={() => handleCreatePostModal('Discussion', true)}
>
{info?.icon}
{info?.label}
</span>
))}
</Box>
</Box>
) : null}
<PostFeed groupId={groupId} postType={postType} ruleId={ruleId} />
{showCreatePostModal ? (
<CreateEngagementPost
open={showCreatePostModal}
onCloseModal={() => {
handleCreatePostModal('Discussion', false);
reset();
}}
title={watch('title', '')}
onSubmit={onSubmit}
item={item}
/>
) : null}
{showCreateSalePostModal ? (
<CreateSalePost
open={showCreateSalePostModal}
onCloseModal={() => handleCreatePostModal('Sale', false)}
title={watch('title', '')}
onSubmit={onSubmit}
item={item}
/>
) : null}
</div>
);
};
export default CommonGroupItemsList;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment