Skip to content
Merged
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/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
# @baseapp-frontend/components

## 1.0.34

### Patch Changes
- Content Feed creation page and Dropzone Improvements
- Updated dependencies
- @baseapp-frontend/design-system@1.0.15

## 1.0.33

### Patch Changes
Expand Down
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
user {
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 { ContentFeedProps } from './types'

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

const onNewPost = useCallback(() => {
router.push('/posts/new')
}, [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 @@
export interface ContentFeedProps {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
'use client'

import { FC, useState } from 'react'

import { Dropzone } from '@baseapp-frontend/design-system/components/web/dropzones'

import { Box, Typography } from '@mui/material'
import Image from 'next/image'

import { IContentFeedImageProps } from './types'

const ContentFeedImage: FC<IContentFeedImageProps> = ({ form }) => {
const [selectedPreview, setSelectedPreview] = useState<File>()
const [selectedPreviewIndex, setSelectedPreviewIndex] = useState<number>()

const { watch } = form

const formFiles = watch('images')

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

if (selectedPreviewIndex === fileIndex) {
setSelectedPreview(undefined)
setSelectedPreviewIndex(undefined)
}
}

const onSelect = (files: (File | Blob)[]) => {
if (files.length) {
form.setValue('images', [...formFiles, ...(files as File[])])
}
}

return (
<Box>
{selectedPreview && (
<Box width="100%" position="relative" height="500px" mb="24px">
<Image
src={URL.createObjectURL(selectedPreview)}
alt={selectedPreview.name}
fill
style={{ objectFit: 'cover', borderRadius: '8px', height: '100%', width: '100%' }}
onLoad={() => URL.revokeObjectURL(URL.createObjectURL(selectedPreview))}
/>
</Box>
)}
<Dropzone
onSelect={onSelect}
includeActionButton={false}
maxFileSize={15}
onRemove={handleRemoveFile}
InputContainerStyle={{ backgroundColor: 'transparent' }}
asBase64={false}
multiple
accept={{ 'image/png': ['.png'], 'image/gif': ['.gif'], 'image/jpeg': ['.jpg', '.jpeg'] }}
title={
<Typography variant="body2" sx={{ color: 'text.secondary' }}>
Click to browse or drag and drop images and videos.
</Typography>
}
onFileClick={(selectedFile, index) => {
setSelectedPreview(selectedFile as File)
setSelectedPreviewIndex(index)
}}
/>
</Box>
)
}

export default ContentFeedImage
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: File[]
},
any,
undefined
>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { z } from 'zod'

import { ContentPostCreateForm } from './types'

export const DEFAULT_CONTENT_POST_CREATE_FORM_VALUES = {
content: '',
images: [] as File[],
}

export const CONTENT_POST_CREATE_FORM_VALIDATION = z.object({
content: z.string(),
images: z.array(z.instanceof(File)),
} satisfies Record<keyof ContentPostCreateForm, unknown>)
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
'use client'

import { FC, useCallback } from 'react'

import { TextField } from '@baseapp-frontend/design-system/components/web/inputs'
import { setFormRelayErrors, useNotification } from '@baseapp-frontend/utils'

import { zodResolver } from '@hookform/resolvers/zod'
import LoadingButton from '@mui/lab/LoadingButton'
import { Box, Button, Typography } from '@mui/material'
import { useRouter } from 'next/navigation'
import { useForm } from 'react-hook-form'

import { useContentPostCreateMutation } from '../../common/graphql/mutations/ContentPostCreate'
import ContentFeedImage from '../ContentFeedImage'
import {
CONTENT_POST_CREATE_FORM_VALIDATION,
DEFAULT_CONTENT_POST_CREATE_FORM_VALUES,
} from './constants'
import { ButtonContainer, HeaderContainer, RootContainer } from './styled'
import { ContentPostCreateForm, NewContentPostProps, UploadableContentPostFiles } from './types'

const NewContentPost: FC<NewContentPostProps> = () => {
const router = useRouter()
const { sendToast } = useNotification()
const [commitMutation, isMutationInFlight] = useContentPostCreateMutation()

const formReturn = useForm({
defaultValues: DEFAULT_CONTENT_POST_CREATE_FORM_VALUES,
resolver: zodResolver(CONTENT_POST_CREATE_FORM_VALIDATION),
mode: 'onBlur',
})

const {
control,
handleSubmit,
reset,
formState: { isDirty, isValid },
} = formReturn

const onSubmit = handleSubmit((data: ContentPostCreateForm) => {
const uploadables: UploadableContentPostFiles = {}

if (data.images) {
data.images.forEach((image, index) => {
uploadables[`images.${index}`] = image as File
})
}

commitMutation({
variables: {
input: {
content: data.content,
},
},
uploadables,
onCompleted(response) {
const errors = response.contentPostCreate?.errors
if (errors) {
sendToast('Something went wrong', { type: 'error' })
setFormRelayErrors(formReturn, errors)
} else {
reset({ content: '' })
sendToast('Post Created Successfully', { type: 'success' })
router.push(`/posts/${response.contentPostCreate?.contentPost?.node?.id}`)
}
},
})
})

const onCancel = useCallback(() => {
router.push('/posts')
}, [router])

return (
<RootContainer>
<form onSubmit={onSubmit}>
<HeaderContainer>
<Typography component="h4" variant="h4">
New Post
</Typography>
<ButtonContainer>
<Button variant="outlined" color="inherit" onClick={onCancel} disableRipple>
Cancel
</Button>
<LoadingButton
color="inherit"
type="submit"
loading={isMutationInFlight}
disabled={!isDirty || !isValid || isMutationInFlight}
sx={{ maxWidth: 'fit-content', justifySelf: 'end' }}
>
Publish
</LoadingButton>
</ButtonContainer>
</HeaderContainer>
<ContentFeedImage form={formReturn} />
<Box>
<TextField
name="content"
type="text"
placeholder="What is on your mind?"
multiline
rows={4}
control={control}
/>
</Box>
</form>
</RootContainer>
)
}

export default NewContentPost
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',
marginBottom: '32px',
}))

export const ButtonContainer = styled(Box)(() => ({
display: 'flex',
width: 'fit-content',
flexDirection: 'row',
gap: '10px',
}))
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
export interface ContentPostCreateForm {
content: string
images?: File[]
}

export type UploadableContentPostFiles = {
[key: `images.${number}`]: File | Blob
}

export type NewContentPostProps = Record<string, never>
10 changes: 10 additions & 0 deletions packages/components/modules/content-feed/web/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// exports content feed components

export { default as ContentFeed } from './ContentFeed'
export type * from './ContentFeed/types'

export { default as ContentFeedImage } from './ContentFeedImage'
export type * from './ContentFeedImage/types'

export { default as NewContentPost } from './NewContentPost'
export type * from './NewContentPost/types'
7 changes: 6 additions & 1 deletion packages/components/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@baseapp-frontend/components",
"description": "BaseApp components modules such as comments, notifications, messages, and more.",
"version": "1.0.33",
"version": "1.0.34",
"sideEffects": false,
"scripts": {
"babel:transpile": "babel modules -d tmp-babel --extensions .ts,.tsx --ignore '**/__tests__/**','**/__storybook__/**'",
Expand Down Expand Up @@ -65,6 +65,11 @@
"types": "./dist/tests/*/index.d.ts",
"import": "./dist/tests/*/index.mjs",
"require": "./dist/tests/*/index.js"
},
"./content-feed/*": {
"types": "./dist/content-feed/*/index.d.ts",
"import": "./dist/content-feed/*/index.mjs",
"require": "./dist/content-feed/*/index.js"
}
},
"files": [
Expand Down
Loading
Loading