-
Notifications
You must be signed in to change notification settings - Fork 1
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
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
export * from './modules/__shared__' | ||
export * from './modules/comments' | ||
export * from './modules/messages' | ||
export * from './modules/navigations' | ||
export * from './modules/content-feed' | ||
export * from './modules/profiles' | ||
export * from './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 { | ||
} | ||
} | ||
} | ||
errors { | ||
field | ||
messages | ||
} | ||
} | ||
} | ||
` | ||
|
||
export const useContentPostCreateMutation = () => | ||
useMutation<ContentPostCreateMutation>(ContentPostCreateMutationQuery) |
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') | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||
}, [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
|
||||||||||
import {CloseRounded as CloseRoundedIcon} from '@mui/icons-material' | ||||||||||
Check failure on line 8 in packages/components/modules/content-feed/web/ContentFeedImage/index.tsx
|
||||||||||
import {InsertPhotoOutlined as InsertPhotoOutlinedIcon} from '@mui/icons-material' | ||||||||||
Check failure on line 9 in packages/components/modules/content-feed/web/ContentFeedImage/index.tsx
|
||||||||||
Comment on lines
+7
to
+9
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 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
Suggested change
🧰 Tools🪛 GitHub Check: Build and Lint Packages[failure] 9-9: [failure] 8-8: [failure] 7-7: |
||||||||||
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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 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) => { | ||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. lets import |
||||||||||
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[])]) | ||||||||||
} | ||||||||||
} | ||||||||||
coderabbitai[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||
|
||||||||||
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> | ||||||||||
)} | ||||||||||
coderabbitai[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||
|
||||||||||
{(isDragging || !formFiles?.length) && ( | ||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 💡 Verification agent 🧩 Analysis chainConsider 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:
💡 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 PrincipleComponents should handle one primary task (e.g., rendering a button, displaying data).
2. Props-Driven CustomizationUse props to make components adaptable:
// 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 EffectsExtract 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
const InputField = ({ value, onChange, label }) => (
<div>
<label>{label}</label>
<input value={value} onChange={onChange} />
</div>
); 5. Component CompositionCompose smaller components into complex UIs:
[3][6] 6. Default Props and Type Checking
7. Styling Flexibility
8. Custom Hooks for Reusable LogicExtract 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
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
|
||||||||||
)} | ||||||||||
|
||||||||||
<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], | ||
}, | ||
})) | ||
coderabbitai[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
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[] | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Reconsider using Using 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
Suggested change
|
||||||
}, | ||||||
any, | ||||||
undefined | ||||||
> | ||||||
} |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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