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

Content Feed New Post #209

Open
wants to merge 1 commit into
base: master
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
7 changes: 7 additions & 0 deletions packages/components/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export * from './modules/__shared__'

Check failure on line 1 in packages/components/index.ts

View workflow job for this annotation

GitHub Actions / Build and Lint Packages

Missing file extension for "./modules/__shared__"
export * from './modules/comments'

Check failure on line 2 in packages/components/index.ts

View workflow job for this annotation

GitHub Actions / Build and Lint Packages

Missing file extension for "./modules/comments"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this file can be removed.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes @rodrigonahid please remove it

export * from './modules/messages'

Check failure on line 3 in packages/components/index.ts

View workflow job for this annotation

GitHub Actions / Build and Lint Packages

Missing file extension for "./modules/messages"
export * from './modules/navigations'

Check failure on line 4 in packages/components/index.ts

View workflow job for this annotation

GitHub Actions / Build and Lint Packages

Missing file extension for "./modules/navigations"
export * from './modules/content-feed'

Check failure on line 5 in packages/components/index.ts

View workflow job for this annotation

GitHub Actions / Build and Lint Packages

Missing file extension for "./modules/content-feed"
export * from './modules/profiles'

Check failure on line 6 in packages/components/index.ts

View workflow job for this annotation

GitHub Actions / Build and Lint Packages

Missing file extension for "./modules/profiles"
export * from './modules/notifications'

Check failure on line 7 in packages/components/index.ts

View workflow job for this annotation

GitHub Actions / Build and Lint Packages

Missing file extension for "./modules/notifications"
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { graphql, useMutation } from 'react-relay'

import { ContentPostCreateMutation } from '../../../../../__generated__/ContentPostCreateMutation.graphql'

export const ContentPostCreateMutationQuery = graphql`
mutation ContentPostCreateMutation($input: ContentPostCreateInput!) {
contentPostCreate(input: $input) {
contentPost {
node {
id
content
author {
email
}
}
}
errors {
field
messages
}
}
}
`

export const useContentPostCreateMutation = () =>
useMutation<ContentPostCreateMutation>(ContentPostCreateMutationQuery)
3 changes: 3 additions & 0 deletions packages/components/modules/content-feed/common/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
// exports common content-feed code

export * from './graphql/mutations/ContentPostCreate'
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
'use client'

import { FC, useCallback } from 'react'

import { Button, Typography } from '@mui/material'
import { useRouter } from 'next/navigation'

import { HeaderContainer, RootContainer } from './styled'
import { IContentFeedProps } from './types'

const ContentFeed: FC<IContentFeedProps> = () => {
const router = useRouter()

const onNewPost = useCallback(() => {
router.push('/new-post')
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we make this route configurable? just in case clients need this in different routes. We can keep using '/new-post' as default

}, [router])

return (
<RootContainer>
<HeaderContainer>
<Typography component="h4" variant="h4">
Content Feed
</Typography>
<Button
variant="outlined"
color="inherit"
onClick={onNewPost}
disableRipple
sx={{ maxWidth: 'fit-content' }}
>
New Post
</Button>
</HeaderContainer>
</RootContainer>
)
}

export default ContentFeed
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { Box, styled } from '@mui/material'

export const RootContainer = styled(Box)(() => ({
display: 'flex',
width: '600px',
alignSelf: 'center',
flexDirection: 'column',
}))

export const HeaderContainer = styled(Box)(() => ({
display: 'flex',
width: '100%',
alignSelf: 'center',
flexDirection: 'row',
justifyContent: 'space-between',
}))

export const ButtonContainer = styled(Box)(() => ({
display: 'flex',
width: 'fit-content',
flexDirection: 'row',
justifyContent: 'space-between',
gap: '10px',
}))
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export interface IContentFeedProps {

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
'use client'

import { ChangeEventHandler, FC, useRef, useState } from 'react'

import { useNotification } from '@baseapp-frontend/utils'

import {AddRounded as AddRoundedIcon} from '@mui/icons-material'

Check failure on line 7 in packages/components/modules/content-feed/web/ContentFeedImage/index.tsx

View workflow job for this annotation

GitHub Actions / Build and Lint Packages

'/home/runner/work/baseapp-frontend/baseapp-frontend/packages/components/node_modules/@mui/icons-material/esm/index.js' imported multiple times
import {CloseRounded as CloseRoundedIcon} from '@mui/icons-material'

Check failure on line 8 in packages/components/modules/content-feed/web/ContentFeedImage/index.tsx

View workflow job for this annotation

GitHub Actions / Build and Lint Packages

'/home/runner/work/baseapp-frontend/baseapp-frontend/packages/components/node_modules/@mui/icons-material/esm/index.js' imported multiple times
import {InsertPhotoOutlined as InsertPhotoOutlinedIcon} from '@mui/icons-material'

Check failure on line 9 in packages/components/modules/content-feed/web/ContentFeedImage/index.tsx

View workflow job for this annotation

GitHub Actions / Build and Lint Packages

'/home/runner/work/baseapp-frontend/baseapp-frontend/packages/components/node_modules/@mui/icons-material/esm/index.js' imported multiple times
Comment on lines +7 to +9
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Fix imports from @mui/icons-material.

The current import approach is causing build failures due to multiple imports from the same module. Consider using a single import statement.

-import {AddRounded as AddRoundedIcon} from '@mui/icons-material'
-import {CloseRounded as CloseRoundedIcon} from '@mui/icons-material'
-import {InsertPhotoOutlined as InsertPhotoOutlinedIcon} from '@mui/icons-material'
+import { AddRounded as AddRoundedIcon, CloseRounded as CloseRoundedIcon, InsertPhotoOutlined as InsertPhotoOutlinedIcon } from '@mui/icons-material'
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
import {AddRounded as AddRoundedIcon} from '@mui/icons-material'
import {CloseRounded as CloseRoundedIcon} from '@mui/icons-material'
import {InsertPhotoOutlined as InsertPhotoOutlinedIcon} from '@mui/icons-material'
import { AddRounded as AddRoundedIcon, CloseRounded as CloseRoundedIcon, InsertPhotoOutlined as InsertPhotoOutlinedIcon } from '@mui/icons-material'
🧰 Tools
🪛 GitHub Check: Build and Lint Packages

[failure] 9-9:
'/home/runner/work/baseapp-frontend/baseapp-frontend/packages/components/node_modules/@mui/icons-material/esm/index.js' imported multiple times


[failure] 8-8:
'/home/runner/work/baseapp-frontend/baseapp-frontend/packages/components/node_modules/@mui/icons-material/esm/index.js' imported multiple times


[failure] 7-7:
'/home/runner/work/baseapp-frontend/baseapp-frontend/packages/components/node_modules/@mui/icons-material/esm/index.js' imported multiple times

import { Box, Typography } from '@mui/material'
import Image from 'next/image'
import { Controller } from 'react-hook-form'

import {
AddFileButton,
AddFileWrapper,
ContentFeedImageContainer,
DropFilesContainer,
MiniatureFileWrapper,
RemoveFileButton,
} from './styled'
import { IContentFeedImageProps } from './types'

const ContentFeedImage: FC<IContentFeedImageProps> = ({ form }) => {
const [selectedUploadedFile, setSelectedUploadedFiles] = useState<File>()
const [isDragging, setIsDragging] = useState(false)

const DEFAULT_IMAGE_FORMATS = 'image/png, image/gif, image/jpeg'
const DEFAULT_IMAGE_MAX_SIZE = 10 * 1024 * 1024
Comment on lines +28 to +29
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Fix inconsistency in max file size.

The displayed max file size (15MB) doesn't match the actual validation limit (10MB).

-  const DEFAULT_IMAGE_MAX_SIZE = 10 * 1024 * 1024
+  const DEFAULT_IMAGE_MAX_SIZE = 15 * 1024 * 1024

Also applies to: 110-112


const fileRef = useRef<HTMLInputElement>(null)
const { sendToast } = useNotification()

const { control, watch } = form

const formFiles: File[] = watch('images')

const handleRemoveFile = (fileIndex: number) => {
const updatedFiles = formFiles?.filter((_, index) => index !== fileIndex)
form.setValue('images', updatedFiles as never, { shouldValidate: true })
}

const handleDragEnter = (e: React.DragEvent) => {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lets import DragEvent directly, right now it's undefined

e.preventDefault()
e.stopPropagation()
setIsDragging(true)
}

const handleDragOver = (e: React.DragEvent) => {
e.preventDefault()
e.stopPropagation()
}

const handleDragLeave = (e: React.DragEvent) => {
e.preventDefault()
e.stopPropagation()
if (!e.currentTarget.contains(e.relatedTarget as Node)) {
setIsDragging(false)
}
}

const handleDrop = (e: React.DragEvent) => {
e.preventDefault()
e.stopPropagation()
setIsDragging(false)

const { files } = e.dataTransfer
if (files.length) {
form.setValue('images', [...(formFiles as never[]), ...(files as unknown as never[])])
}
}

return (
<Box
onDragEnter={handleDragEnter}
onDragOver={handleDragOver}
onDragLeave={handleDragLeave}
onDrop={handleDrop}
>
{selectedUploadedFile && !isDragging && (
<Box width="100%" position="relative" height="500px" mb="24px">
<Image
src={URL.createObjectURL(selectedUploadedFile)}
alt={selectedUploadedFile.name}
fill
style={{ objectFit: 'cover', borderRadius: '8px', height: '100%', width: '100%' }}
onLoad={() => URL.revokeObjectURL(URL.createObjectURL(selectedUploadedFile))}
/>
</Box>
)}

{(isDragging || !formFiles?.length) && (
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if we could've reused the design-system Dropzone component

<DropFilesContainer onClick={() => fileRef?.current?.click()}>
<InsertPhotoOutlinedIcon
sx={{ width: '36px', height: '36px', marginBottom: '4px', color: 'text.secondary' }}
/>
<Typography variant="body2" sx={{ color: 'text.secondary' }}>
Click to browse or drag and drop images and videos.
</Typography>
<Typography variant="caption" sx={{ color: 'text.secondary' }}>
Max. File Size: 15MB
</Typography>
</DropFilesContainer>
Comment on lines +92 to +103
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Consider using the existing Dropzone component.

The design system already has a Dropzone component that could be reused here, promoting consistency and reducing code duplication.


🌐 Web query:

What are the best practices for component reuse in React applications?

💡 Result:

To build maintainable and scalable React applications, adhering to component reuse best practices is critical. Below are the most effective strategies, supported by industry standards and practical examples:


1. Single Responsibility Principle

Components should handle one primary task (e.g., rendering a button, displaying data).

  • Example: Split a monolithic TodoApp into smaller components (TodoInput, TodoList, TodoItem) to avoid mixing state management, UI rendering, and event handling[2][3].
  • Avoid components that manage both API calls and UI rendering[1][4].

2. Props-Driven Customization

Use props to make components adaptable:

  • Core Props: Pass onClick, color, and label to customize behavior and appearance[4][5].
  • Children Prop: Allow content injection (e.g., <Button>Submit</Button>)[7][9].
// Reusable Button Component
const Button = ({ label, onClick, color = "blue" }) => (
  <button 
    className={`bg-${color}-500 hover:bg-${color}-700`} 
    onClick={onClick}
  >
    {label}
  </button>
);

3. Avoid Side Effects

Extract logic like API calls or local storage interactions into parent components or custom hooks:

// Anti-pattern: Side effect inside component ❌
const SaveButton = () => {
  const handleClick = () => { /* API call */ };
  return <button onClick={handleClick}>Save</button>;
};

// Best Practice: Pass handler via props ✅
const SaveButton = ({ onClick }) => (
  <button onClick={onClick}>Save</button>
);

[1][4][5]


4. State Management

  • Stateless Components: Keep components "dumb" by managing state externally[7][10].
  • Controlled Components: Use props like value and onChange for inputs[10]:
const InputField = ({ value, onChange, label }) => (
  <div>
    <label>{label}</label>
    <input value={value} onChange={onChange} />
  </div>
);

5. Component Composition

Compose smaller components into complex UIs:

  • Atomic Design: Structure components as atoms (buttons), molecules (forms), and organisms (navbars)[2][11].
  • Project Structure:
    src/
      components/
        Button/
          index.jsx
          styles.css
        Form/
          Input.jsx
          Select.jsx
    

[3][6]


6. Default Props and Type Checking

  • Default Values: Ensure fallback behavior with defaultProps[9]:
    Button.defaultProps = { color: "blue", label: "Click Me" };
  • PropTypes: Validate props for reliability[11]:
    import PropTypes from 'prop-types';
    Button.propTypes = {
      label: PropTypes.string,
      onClick: PropTypes.func.isRequired,
    };

7. Styling Flexibility

  • CSS Modules/Styled Components: Scope styles to components[7].
  • Classname Libraries: Use utilities like clsx for dynamic classes[8].

8. Custom Hooks for Reusable Logic

Extract logic like data fetching into hooks:

const useFetch = (url) => {
  const [data, setData] = useState(null);
  useEffect(() => {
    fetch(url).then(res => setData(res.json()));
  }, [url]);
  return data;
};

[3][10]


9. Documentation and Testing

  • Storybook: Document component variations.
  • Unit Tests: Ensure components behave as expected with different props[11].

By following these practices, you’ll create components that are modular, testable, and scalable, reducing redundancy and improving maintainability across projects.

Citations:


Action Item: Switch to Design System’s Dropzone Component

Please refactor the inline drop area (lines 92–103 in packages/components/modules/content-feed/web/ContentFeedImage/index.tsx) to use the existing Dropzone component. This change will enhance UI consistency and promote reusability in line with React best practices such as the single responsibility principle and props-driven customization.

  • Replace the current inline implementation with the standardized Dropzone.
  • Ensure that all necessary functionality (e.g., click handler to trigger file selection and drag-and-drop support) is properly mapped via props.
  • Verify that file size limitations and UI styling match the requirements.

)}

<ContentFeedImageContainer gap={0.75}>
{!!formFiles?.length && (
<AddFileWrapper>
<AddFileButton color="inherit" onClick={() => fileRef?.current?.click()} disableRipple>
<AddRoundedIcon sx={{ width: '28px', height: '28px', color: 'text.primary' }} />
</AddFileButton>
</AddFileWrapper>
)}
<Controller
name="images"
control={control}
render={({ field }) => {
const handleOnChange: ChangeEventHandler<HTMLInputElement | HTMLTextAreaElement> = (
event,
) => {
const { files } = event.target as HTMLInputElement

if (files) {
// Convert FileList to an array of File objects
const filesArray = Array.from(files)

// Filter and process valid files
const validFiles = filesArray.filter((file) => {
if (file.size > DEFAULT_IMAGE_MAX_SIZE) {
// Notify the user if the file is too large
sendToast(
`This file is too large (max ${DEFAULT_IMAGE_MAX_SIZE / 1024 / 1024}MB).`,
{
type: 'error',
},
)
return false // Exclude this file
}
return true // Include this file
})

// Update the form field with the valid files
if (validFiles.length > 0) {
field.onChange([...formFiles, ...validFiles])
}
}
}
return (
<input
onChange={handleOnChange}
type="file"
multiple
ref={fileRef}
accept={DEFAULT_IMAGE_FORMATS}
style={{ display: 'none' }}
/>
)
}}
/>
{formFiles?.map((file, index) => (
<MiniatureFileWrapper key={`${file.name}`}>
<button
style={{ height: '100%' }}
type="button"
onClick={() => setSelectedUploadedFiles(file)}
>
<Image
src={URL.createObjectURL(file)}
alt={file.name}
width={72}
height={72}
style={{ objectFit: 'cover', borderRadius: '8px', height: '100%' }}
onLoad={() => URL.revokeObjectURL(URL.createObjectURL(file))}
/>
</button>
<RemoveFileButton onClick={() => handleRemoveFile(index)}>
<CloseRoundedIcon sx={{ color: 'white', width: '20px', height: '20px' }} />
</RemoveFileButton>
</MiniatureFileWrapper>
))}
</ContentFeedImageContainer>
</Box>
)
}

export default ContentFeedImage
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import { Box, Button, styled } from '@mui/material'

export const ContentFeedImageContainer = styled(Box)(({ theme }) => ({
display: 'flex',
marginBottom: '16px',
overflow: 'auto',
paddingBottom: '6px',
'::-webkit-scrollbar': {
height: '6px',
},
'::-webkit-scrollbar-track': {
boxShadow: `inset 0 0 1px ${theme.palette.grey[400]}`,
borderRadius: '10px',
},
'::-webkit-scrollbar-thumb': {
background: theme.palette.grey[400],
borderRadius: '10px',
},
'::-webkit-scrollbar-thumb:hover': {
background: theme.palette.grey[600],
},
}))

export const AddFileButton = styled(Button)(({ theme }) => ({
width: '72px',
height: '72px',
backgroundColor: theme.palette.grey[200],
borderRadius: '8px',
'&:hover': {
backgroundColor: theme.palette.grey[300],
},
}))

export const AddFileWrapper = styled(Box)(({ theme }) => ({
border: `2px dashed ${theme.palette.grey[200]}`,
borderRadius: '12px',
padding: '4px',
display: 'inline-block',
flexShrink: 0,
}))

export const MiniatureFileWrapper = styled(Box)(({ theme }) => ({
position: 'relative',
flexShrink: 0,
width: '80px',
height: '80px',
border: `2px solid ${theme.palette.grey[200]}`,
borderRadius: '12px',
padding: '4px',
display: 'inline-block',
'&:hover': {
border: `2px solid black`,
},
}))

export const RemoveFileButton = styled('button')(({ theme }) => ({
position: 'absolute',
top: '4px',
right: '4px',
width: '28px',
height: '28px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
borderRadius: '0 6px 0 6px',
backgroundColor: theme.palette.grey[800],
'&:hover': {
backgroundColor: theme.palette.grey[800],
},
}))

export const DropFilesContainer = styled(Box)(({ theme }) => ({
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
flexDirection: 'column',
border: `1px dashed ${theme.palette.grey[400]}`,
borderImageSlice: 1,
width: '100%',
height: '144px',
borderRadius: '8px',
marginBottom: '8px',
}))
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { UseFormReturn } from "react-hook-form"

export interface IContentFeedImageProps {
form: UseFormReturn<
{
content: string
images: never[]
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Reconsider using never[] for images type

Using never[] for the images field type means the array can never contain any valid elements, which seems too restrictive for an image upload feature.

Consider using a more appropriate type for the images array based on what you expect to store:

- images: never[]
+ images: File[] // If you're storing File objects from file inputs

Or if you're storing image metadata or URLs:

- images: never[]
+ images: { url: string; name?: string; size?: number }[]
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
images: never[]
images: File[] // If you're storing File objects from file inputs

},
any,
undefined
>
}
Loading
Loading