diff --git a/packages/components/CHANGELOG.md b/packages/components/CHANGELOG.md index 33beea18..8d9bdb24 100644 --- a/packages/components/CHANGELOG.md +++ b/packages/components/CHANGELOG.md @@ -1,5 +1,11 @@ # @baseapp-frontend/components +## 1.2.1 + +### Patch Changes + +- Content Feed Posts list with images and reactions + ## 1.2.0 ### Minor Changes diff --git a/packages/components/__generated__/ActivityLogsFragment.graphql.ts b/packages/components/__generated__/ActivityLogsFragment.graphql.ts index cc07aaf0..126d28b5 100644 --- a/packages/components/__generated__/ActivityLogsFragment.graphql.ts +++ b/packages/components/__generated__/ActivityLogsFragment.graphql.ts @@ -5,351 +5,328 @@ */ /* tslint:disable */ - /* eslint-disable */ // @ts-nocheck -import { ReaderFragment } from 'relay-runtime' -import { FragmentRefs } from 'relay-runtime' - -import ActivityLogsPaginationQuery_graphql from './ActivityLogsPaginationQuery.graphql' +import { ReaderFragment } from 'relay-runtime'; +import { FragmentRefs } from "relay-runtime"; export type ActivityLogsFragment$data = { - readonly activityLogs: - | { - readonly edges: ReadonlyArray< - | { - readonly node: - | { - readonly createdAt: any - readonly events: - | { - readonly edges: ReadonlyArray< - | { - readonly node: - | { - readonly diff: any | null | undefined - readonly label: string | null | undefined - } - | null - | undefined - } - | null - | undefined - > - } - | null - | undefined - readonly id: string - readonly url: string | null | undefined - readonly user: - | { - readonly avatar: - | { - readonly url: string - } - | null - | undefined - readonly email: string | null | undefined - readonly fullName: string | null | undefined - readonly id: string - } - | null - | undefined - readonly verb: string | null | undefined - } - | null - | undefined - } - | null - | undefined - > - readonly pageInfo: { - readonly endCursor: string | null | undefined - readonly hasNextPage: boolean - } - } - | null - | undefined - readonly ' $fragmentType': 'ActivityLogsFragment' -} + readonly activityLogs: { + readonly edges: ReadonlyArray<{ + readonly node: { + readonly createdAt: any; + readonly events: { + readonly edges: ReadonlyArray<{ + readonly node: { + readonly diff: any | null | undefined; + readonly label: string | null | undefined; + } | null | undefined; + } | null | undefined>; + } | null | undefined; + readonly id: string; + readonly url: string | null | undefined; + readonly user: { + readonly avatar: { + readonly url: string; + } | null | undefined; + readonly email: string | null | undefined; + readonly fullName: string | null | undefined; + readonly id: string; + } | null | undefined; + readonly verb: string | null | undefined; + } | null | undefined; + } | null | undefined>; + readonly pageInfo: { + readonly endCursor: string | null | undefined; + readonly hasNextPage: boolean; + }; + } | null | undefined; + readonly " $fragmentType": "ActivityLogsFragment"; +}; export type ActivityLogsFragment$key = { - readonly ' $data'?: ActivityLogsFragment$data - readonly ' $fragmentSpreads': FragmentRefs<'ActivityLogsFragment'> -} + readonly " $data"?: ActivityLogsFragment$data; + readonly " $fragmentSpreads": FragmentRefs<"ActivityLogsFragment">; +}; + +import ActivityLogsPaginationQuery_graphql from './ActivityLogsPaginationQuery.graphql'; -const node: ReaderFragment = (function () { - var v0 = ['activityLogs'], - v1 = { - alias: null, - args: null, - kind: 'ScalarField', - name: 'id', - storageKey: null, +const node: ReaderFragment = (function(){ +var v0 = [ + "activityLogs" +], +v1 = { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "id", + "storageKey": null +}, +v2 = { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "url", + "storageKey": null +}; +return { + "argumentDefinitions": [ + { + "defaultValue": 10, + "kind": "LocalArgument", + "name": "count" + }, + { + "defaultValue": null, + "kind": "LocalArgument", + "name": "createdFrom" + }, + { + "defaultValue": null, + "kind": "LocalArgument", + "name": "createdTo" + }, + { + "defaultValue": null, + "kind": "LocalArgument", + "name": "cursor" }, - v2 = { - alias: null, - args: null, - kind: 'ScalarField', - name: 'url', - storageKey: null, + { + "defaultValue": null, + "kind": "LocalArgument", + "name": "userName" } - return { - argumentDefinitions: [ + ], + "kind": "Fragment", + "metadata": { + "connection": [ { - defaultValue: 10, - kind: 'LocalArgument', - name: 'count', - }, - { - defaultValue: null, - kind: 'LocalArgument', - name: 'createdFrom', - }, - { - defaultValue: null, - kind: 'LocalArgument', - name: 'createdTo', - }, - { - defaultValue: null, - kind: 'LocalArgument', - name: 'cursor', - }, - { - defaultValue: null, - kind: 'LocalArgument', - name: 'userName', - }, + "count": "count", + "cursor": "cursor", + "direction": "forward", + "path": (v0/*: any*/) + } ], - kind: 'Fragment', - metadata: { - connection: [ + "refetch": { + "connection": { + "forward": { + "count": "count", + "cursor": "cursor" + }, + "backward": null, + "path": (v0/*: any*/) + }, + "fragmentPathInResult": [], + "operation": ActivityLogsPaginationQuery_graphql + } + }, + "name": "ActivityLogsFragment", + "selections": [ + { + "alias": "activityLogs", + "args": [ { - count: 'count', - cursor: 'cursor', - direction: 'forward', - path: v0 /*: any*/, + "kind": "Variable", + "name": "createdFrom", + "variableName": "createdFrom" }, + { + "kind": "Variable", + "name": "createdTo", + "variableName": "createdTo" + }, + { + "kind": "Variable", + "name": "userName", + "variableName": "userName" + } ], - refetch: { - connection: { - forward: { - count: 'count', - cursor: 'cursor', - }, - backward: null, - path: v0 /*: any*/, + "concreteType": "ActivityLogConnection", + "kind": "LinkedField", + "name": "__ActivityLogs_activityLogs_connection", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "ActivityLogEdge", + "kind": "LinkedField", + "name": "edges", + "plural": true, + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "ActivityLog", + "kind": "LinkedField", + "name": "node", + "plural": false, + "selections": [ + (v1/*: any*/), + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "createdAt", + "storageKey": null + }, + { + "alias": null, + "args": null, + "concreteType": "NodeLogEventConnection", + "kind": "LinkedField", + "name": "events", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "NodeLogEventEdge", + "kind": "LinkedField", + "name": "edges", + "plural": true, + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "NodeLogEvent", + "kind": "LinkedField", + "name": "node", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "label", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "diff", + "storageKey": null + } + ], + "storageKey": null + } + ], + "storageKey": null + } + ], + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "verb", + "storageKey": null + }, + (v2/*: any*/), + { + "alias": null, + "args": null, + "concreteType": "User", + "kind": "LinkedField", + "name": "user", + "plural": false, + "selections": [ + (v1/*: any*/), + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "fullName", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "email", + "storageKey": null + }, + { + "alias": null, + "args": [ + { + "kind": "Literal", + "name": "height", + "value": 48 + }, + { + "kind": "Literal", + "name": "width", + "value": 48 + } + ], + "concreteType": "File", + "kind": "LinkedField", + "name": "avatar", + "plural": false, + "selections": [ + (v2/*: any*/) + ], + "storageKey": "avatar(height:48,width:48)" + } + ], + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "__typename", + "storageKey": null + } + ], + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "cursor", + "storageKey": null + } + ], + "storageKey": null }, - fragmentPathInResult: [], - operation: ActivityLogsPaginationQuery_graphql, - }, - }, - name: 'ActivityLogsFragment', - selections: [ - { - alias: 'activityLogs', - args: [ - { - kind: 'Variable', - name: 'createdFrom', - variableName: 'createdFrom', - }, - { - kind: 'Variable', - name: 'createdTo', - variableName: 'createdTo', - }, - { - kind: 'Variable', - name: 'userName', - variableName: 'userName', - }, - ], - concreteType: 'ActivityLogConnection', - kind: 'LinkedField', - name: '__ActivityLogs_activityLogs_connection', - plural: false, - selections: [ - { - alias: null, - args: null, - concreteType: 'ActivityLogEdge', - kind: 'LinkedField', - name: 'edges', - plural: true, - selections: [ - { - alias: null, - args: null, - concreteType: 'ActivityLog', - kind: 'LinkedField', - name: 'node', - plural: false, - selections: [ - v1 /*: any*/, - { - alias: null, - args: null, - kind: 'ScalarField', - name: 'createdAt', - storageKey: null, - }, - { - alias: null, - args: null, - concreteType: 'NodeLogEventConnection', - kind: 'LinkedField', - name: 'events', - plural: false, - selections: [ - { - alias: null, - args: null, - concreteType: 'NodeLogEventEdge', - kind: 'LinkedField', - name: 'edges', - plural: true, - selections: [ - { - alias: null, - args: null, - concreteType: 'NodeLogEvent', - kind: 'LinkedField', - name: 'node', - plural: false, - selections: [ - { - alias: null, - args: null, - kind: 'ScalarField', - name: 'label', - storageKey: null, - }, - { - alias: null, - args: null, - kind: 'ScalarField', - name: 'diff', - storageKey: null, - }, - ], - storageKey: null, - }, - ], - storageKey: null, - }, - ], - storageKey: null, - }, - { - alias: null, - args: null, - kind: 'ScalarField', - name: 'verb', - storageKey: null, - }, - v2 /*: any*/, - { - alias: null, - args: null, - concreteType: 'User', - kind: 'LinkedField', - name: 'user', - plural: false, - selections: [ - v1 /*: any*/, - { - alias: null, - args: null, - kind: 'ScalarField', - name: 'fullName', - storageKey: null, - }, - { - alias: null, - args: null, - kind: 'ScalarField', - name: 'email', - storageKey: null, - }, - { - alias: null, - args: [ - { - kind: 'Literal', - name: 'height', - value: 48, - }, - { - kind: 'Literal', - name: 'width', - value: 48, - }, - ], - concreteType: 'File', - kind: 'LinkedField', - name: 'avatar', - plural: false, - selections: [v2 /*: any*/], - storageKey: 'avatar(height:48,width:48)', - }, - ], - storageKey: null, - }, - { - alias: null, - args: null, - kind: 'ScalarField', - name: '__typename', - storageKey: null, - }, - ], - storageKey: null, - }, - { - alias: null, - args: null, - kind: 'ScalarField', - name: 'cursor', - storageKey: null, - }, - ], - storageKey: null, - }, - { - alias: null, - args: null, - concreteType: 'PageInfo', - kind: 'LinkedField', - name: 'pageInfo', - plural: false, - selections: [ - { - alias: null, - args: null, - kind: 'ScalarField', - name: 'endCursor', - storageKey: null, - }, - { - alias: null, - args: null, - kind: 'ScalarField', - name: 'hasNextPage', - storageKey: null, - }, - ], - storageKey: null, - }, - ], - storageKey: null, - }, - ], - type: 'Query', - abstractKey: null, - } -})() + { + "alias": null, + "args": null, + "concreteType": "PageInfo", + "kind": "LinkedField", + "name": "pageInfo", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "endCursor", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "hasNextPage", + "storageKey": null + } + ], + "storageKey": null + } + ], + "storageKey": null + } + ], + "type": "Query", + "abstractKey": null +}; +})(); -;(node as any).hash = '331e3d4312fbe40053110fe98844df70' +(node as any).hash = "331e3d4312fbe40053110fe98844df70"; -export default node +export default node; diff --git a/packages/components/__generated__/RemoveMemberMutation.graphql.ts b/packages/components/__generated__/RemoveMemberMutation.graphql.ts index 9dafb591..a6ccdf3f 100644 --- a/packages/components/__generated__/RemoveMemberMutation.graphql.ts +++ b/packages/components/__generated__/RemoveMemberMutation.graphql.ts @@ -5,115 +5,113 @@ */ /* tslint:disable */ - /* eslint-disable */ // @ts-nocheck -import { ConcreteRequest } from 'relay-runtime' +import { ConcreteRequest } from 'relay-runtime'; export type ProfileRemoveMemberInput = { - clientMutationId?: string | null | undefined - profileId: string - userId: string -} + clientMutationId?: string | null | undefined; + profileId: string; + userId: string; +}; export type RemoveMemberMutation$variables = { - input: ProfileRemoveMemberInput -} + input: ProfileRemoveMemberInput; +}; export type RemoveMemberMutation$data = { - readonly profileRemoveMember: - | { - readonly deletedId: string | null | undefined - } - | null - | undefined -} + readonly profileRemoveMember: { + readonly deletedId: string | null | undefined; + } | null | undefined; +}; export type RemoveMemberMutation = { - response: RemoveMemberMutation$data - variables: RemoveMemberMutation$variables -} + response: RemoveMemberMutation$data; + variables: RemoveMemberMutation$variables; +}; -const node: ConcreteRequest = (function () { - var v0 = [ +const node: ConcreteRequest = (function(){ +var v0 = [ + { + "defaultValue": null, + "kind": "LocalArgument", + "name": "input" + } +], +v1 = [ + { + "kind": "Variable", + "name": "input", + "variableName": "input" + } +], +v2 = { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "deletedId", + "storageKey": null +}; +return { + "fragment": { + "argumentDefinitions": (v0/*: any*/), + "kind": "Fragment", + "metadata": null, + "name": "RemoveMemberMutation", + "selections": [ { - defaultValue: null, - kind: 'LocalArgument', - name: 'input', - }, + "alias": null, + "args": (v1/*: any*/), + "concreteType": "ProfileRemoveMemberPayload", + "kind": "LinkedField", + "name": "profileRemoveMember", + "plural": false, + "selections": [ + (v2/*: any*/) + ], + "storageKey": null + } ], - v1 = [ + "type": "Mutation", + "abstractKey": null + }, + "kind": "Request", + "operation": { + "argumentDefinitions": (v0/*: any*/), + "kind": "Operation", + "name": "RemoveMemberMutation", + "selections": [ { - kind: 'Variable', - name: 'input', - variableName: 'input', - }, - ], - v2 = { - alias: null, - args: null, - kind: 'ScalarField', - name: 'deletedId', - storageKey: null, - } - return { - fragment: { - argumentDefinitions: v0 /*: any*/, - kind: 'Fragment', - metadata: null, - name: 'RemoveMemberMutation', - selections: [ - { - alias: null, - args: v1 /*: any*/, - concreteType: 'ProfileRemoveMemberPayload', - kind: 'LinkedField', - name: 'profileRemoveMember', - plural: false, - selections: [v2 /*: any*/], - storageKey: null, - }, - ], - type: 'Mutation', - abstractKey: null, - }, - kind: 'Request', - operation: { - argumentDefinitions: v0 /*: any*/, - kind: 'Operation', - name: 'RemoveMemberMutation', - selections: [ - { - alias: null, - args: v1 /*: any*/, - concreteType: 'ProfileRemoveMemberPayload', - kind: 'LinkedField', - name: 'profileRemoveMember', - plural: false, - selections: [ - v2 /*: any*/, - { - alias: null, - args: null, - filters: null, - handle: 'deleteRecord', - key: '', - kind: 'ScalarHandle', - name: 'deletedId', - }, - ], - storageKey: null, - }, - ], - }, - params: { - cacheID: '7ede42a17cf2d60398c7a5019de5f013', - id: null, - metadata: {}, - name: 'RemoveMemberMutation', - operationKind: 'mutation', - text: 'mutation RemoveMemberMutation(\n $input: ProfileRemoveMemberInput!\n) {\n profileRemoveMember(input: $input) {\n deletedId\n }\n}\n', - }, + "alias": null, + "args": (v1/*: any*/), + "concreteType": "ProfileRemoveMemberPayload", + "kind": "LinkedField", + "name": "profileRemoveMember", + "plural": false, + "selections": [ + (v2/*: any*/), + { + "alias": null, + "args": null, + "filters": null, + "handle": "deleteRecord", + "key": "", + "kind": "ScalarHandle", + "name": "deletedId" + } + ], + "storageKey": null + } + ] + }, + "params": { + "cacheID": "7ede42a17cf2d60398c7a5019de5f013", + "id": null, + "metadata": {}, + "name": "RemoveMemberMutation", + "operationKind": "mutation", + "text": "mutation RemoveMemberMutation(\n $input: ProfileRemoveMemberInput!\n) {\n profileRemoveMember(input: $input) {\n deletedId\n }\n}\n" } -})() +}; +})(); -;(node as any).hash = '4426831487fa708c1e351d2c7608e1f8' +(node as any).hash = "4426831487fa708c1e351d2c7608e1f8"; -export default node +export default node; diff --git a/packages/components/modules/content-feed/common/graphql/fragments/ContentPost.ts b/packages/components/modules/content-feed/common/graphql/fragments/ContentPost.ts new file mode 100644 index 00000000..1d3da408 --- /dev/null +++ b/packages/components/modules/content-feed/common/graphql/fragments/ContentPost.ts @@ -0,0 +1,24 @@ +import { graphql } from 'react-relay' + +export const ContentPostFragmentQuery = graphql` + fragment ContentPost_post on ContentPost @refetchable(queryName: "ContentPostRefetchQuery") { + id + pk + content + images { + edges { + node { + id + ...ContentPostImageFragment @arguments(width: 600, height: 0) + } + } + } + created + modified + profile { + ...ProfileItemFragment + } + isReactionsEnabled + ...ReactionButton_target + } +` diff --git a/packages/components/modules/content-feed/common/graphql/fragments/ContentPostImage.ts b/packages/components/modules/content-feed/common/graphql/fragments/ContentPostImage.ts new file mode 100644 index 00000000..4e04faa4 --- /dev/null +++ b/packages/components/modules/content-feed/common/graphql/fragments/ContentPostImage.ts @@ -0,0 +1,14 @@ +import { graphql } from 'react-relay' + +export const ContentPostImageFragment = graphql` + fragment ContentPostImageFragment on ContentPostImage + @argumentDefinitions( + width: { type: "Int", defaultValue: 600 } + height: { type: "Int", defaultValue: 0 } + ) { + pk + image(width: $width, height: $height) { + url + } + } +` diff --git a/packages/components/modules/content-feed/common/graphql/fragments/ContentPosts.ts b/packages/components/modules/content-feed/common/graphql/fragments/ContentPosts.ts new file mode 100644 index 00000000..969c1f4d --- /dev/null +++ b/packages/components/modules/content-feed/common/graphql/fragments/ContentPosts.ts @@ -0,0 +1,37 @@ +import { graphql, usePaginationFragment } from 'react-relay' + +import { ContentPostsFragment$key } from '../../../../../__generated__/ContentPostsFragment.graphql' +import { ContentPostsPaginationQuery } from '../../../../../__generated__/ContentPostsPaginationQuery.graphql' + +export const ContentPostsFragmentQuery = graphql` + fragment ContentPostsFragment on Query + @argumentDefinitions( + cursor: { type: "String" } + count: { type: "Int", defaultValue: 5 } + orderBy: { type: "String", defaultValue: "-created" } + ) + @refetchable(queryName: "ContentPostsPaginationQuery") { + contentPosts(first: $count, after: $cursor, orderBy: $orderBy) + @connection(key: "ContentPostsFragment_contentPosts") { + totalCount + edges { + node { + ...ContentPost_post + } + cursor + } + pageInfo { + hasNextPage + hasPreviousPage + endCursor + startCursor + } + } + } +` + +export const useContentPosts = (targetRef: ContentPostsFragment$key) => + usePaginationFragment( + ContentPostsFragmentQuery, + targetRef, + ) diff --git a/packages/components/modules/content-feed/common/graphql/mutations/ContentPostCreate.ts b/packages/components/modules/content-feed/common/graphql/mutations/ContentPostCreate.ts index c9fbfd12..c202ab5f 100644 --- a/packages/components/modules/content-feed/common/graphql/mutations/ContentPostCreate.ts +++ b/packages/components/modules/content-feed/common/graphql/mutations/ContentPostCreate.ts @@ -8,10 +8,7 @@ export const ContentPostCreateMutationQuery = graphql` contentPost { node { id - content - user { - email - } + ...ContentPost_post } } errors { diff --git a/packages/components/modules/content-feed/common/graphql/queries/ContentPostDetail.ts b/packages/components/modules/content-feed/common/graphql/queries/ContentPostDetail.ts new file mode 100644 index 00000000..32a313e8 --- /dev/null +++ b/packages/components/modules/content-feed/common/graphql/queries/ContentPostDetail.ts @@ -0,0 +1,9 @@ +import { graphql } from 'react-relay' + +export const ContentPostDetailFragmentQuery = graphql` + query ContentPostDetailFragmentQuery($id: ID!) { + contentPost(id: $id) { + ...ContentPost_post + } + } +` diff --git a/packages/components/modules/content-feed/common/graphql/queries/ContentPosts.ts b/packages/components/modules/content-feed/common/graphql/queries/ContentPosts.ts new file mode 100644 index 00000000..32aa8ac8 --- /dev/null +++ b/packages/components/modules/content-feed/common/graphql/queries/ContentPosts.ts @@ -0,0 +1,7 @@ +import { graphql } from 'react-relay' + +export const ContentPostsQuery = graphql` + query ContentPostsQuery($count: Int!, $cursor: String, $orderBy: String) { + ...ContentPostsFragment @arguments(count: $count, cursor: $cursor, orderBy: $orderBy) + } +` diff --git a/packages/components/modules/content-feed/common/index.ts b/packages/components/modules/content-feed/common/index.ts index 3f1ebff6..6419761a 100644 --- a/packages/components/modules/content-feed/common/index.ts +++ b/packages/components/modules/content-feed/common/index.ts @@ -1,3 +1,6 @@ // exports common content-feed code export * from './graphql/mutations/ContentPostCreate' +export * from './graphql/fragments/ContentPosts' +export * from './graphql/queries/ContentPosts' +export * from './graphql/queries/ContentPostDetail' diff --git a/packages/components/modules/content-feed/common/types.ts b/packages/components/modules/content-feed/common/types.ts new file mode 100644 index 00000000..7d05b7a4 --- /dev/null +++ b/packages/components/modules/content-feed/common/types.ts @@ -0,0 +1,5 @@ +import { ContentPostsFragment$data } from '../../../__generated__/ContentPostsFragment.graphql' + +export type ContentPosts = NonNullable +export type ContentPostEdges = ContentPosts['edges'] +export type ContentPostNode = NonNullable['node'] diff --git a/packages/components/modules/content-feed/web/ContentFeed/index.tsx b/packages/components/modules/content-feed/web/ContentFeed/index.tsx index 8c8cd228..5180dadb 100644 --- a/packages/components/modules/content-feed/web/ContentFeed/index.tsx +++ b/packages/components/modules/content-feed/web/ContentFeed/index.tsx @@ -5,10 +5,11 @@ import { FC, useCallback } from 'react' import { Button, Typography } from '@mui/material' import { useRouter } from 'next/navigation' -import { HeaderContainer, RootContainer } from './styled' +import PostList from '../PostList' +import { HeaderContainer, RootContainer } from '../styled' import { ContentFeedProps } from './types' -const ContentFeed: FC = () => { +const ContentFeed: FC = ({ preloadedQuery }) => { const router = useRouter() const onNewPost = useCallback(() => { @@ -31,6 +32,8 @@ const ContentFeed: FC = () => { New Post + + ) } diff --git a/packages/components/modules/content-feed/web/ContentFeed/types.ts b/packages/components/modules/content-feed/web/ContentFeed/types.ts index e9fcf522..5453d497 100644 --- a/packages/components/modules/content-feed/web/ContentFeed/types.ts +++ b/packages/components/modules/content-feed/web/ContentFeed/types.ts @@ -1 +1,12 @@ -export interface ContentFeedProps {} +import { + ContentPostsFragment$data, + ContentPostsFragment$key, +} from '../../../../__generated__/ContentPostsFragment.graphql' + +export interface ContentFeedProps { + preloadedQuery: ContentPostsFragment$key +} + +export type ContentPosts = NonNullable +export type ContentPostEdges = ContentPosts['edges'] +export type ContentPostNode = NonNullable['node'] diff --git a/packages/components/modules/content-feed/web/ContentFeedImage/types.ts b/packages/components/modules/content-feed/web/ContentFeedImage/types.ts deleted file mode 100644 index 7eda3e52..00000000 --- a/packages/components/modules/content-feed/web/ContentFeedImage/types.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { UseFormReturn } from 'react-hook-form' - -type ContentFeedForm = { - content: string - images: File[] -} - -export interface IContentFeedImageProps { - form: UseFormReturn -} diff --git a/packages/components/modules/content-feed/web/NewContentPost/constants.ts b/packages/components/modules/content-feed/web/NewContentPost/constants.ts deleted file mode 100644 index fb8d4ced..00000000 --- a/packages/components/modules/content-feed/web/NewContentPost/constants.ts +++ /dev/null @@ -1,13 +0,0 @@ -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) diff --git a/packages/components/modules/content-feed/web/NewContentPost/index.tsx b/packages/components/modules/content-feed/web/NewContentPost/index.tsx deleted file mode 100644 index 23efbbcd..00000000 --- a/packages/components/modules/content-feed/web/NewContentPost/index.tsx +++ /dev/null @@ -1,114 +0,0 @@ -'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 = () => { - 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: { - // @ts-ignore this will be handle on BA-2452 - 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 ( - -
- - - New Post - - - - - Publish - - - - - - - - -
- ) -} - -export default NewContentPost diff --git a/packages/components/modules/content-feed/web/NewContentPost/styled.tsx b/packages/components/modules/content-feed/web/NewContentPost/styled.tsx deleted file mode 100644 index 3ffdb132..00000000 --- a/packages/components/modules/content-feed/web/NewContentPost/styled.tsx +++ /dev/null @@ -1,24 +0,0 @@ -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', -})) diff --git a/packages/components/modules/content-feed/web/NewContentPost/types.ts b/packages/components/modules/content-feed/web/NewContentPost/types.ts deleted file mode 100644 index 5c98118e..00000000 --- a/packages/components/modules/content-feed/web/NewContentPost/types.ts +++ /dev/null @@ -1,10 +0,0 @@ -export interface ContentPostCreateForm { - content: string - images?: File[] -} - -export type UploadableContentPostFiles = { - [key: `images.${number}`]: File | Blob -} - -export type NewContentPostProps = Record diff --git a/packages/components/modules/content-feed/web/PostCreate/index.tsx b/packages/components/modules/content-feed/web/PostCreate/index.tsx new file mode 100644 index 00000000..98303dcb --- /dev/null +++ b/packages/components/modules/content-feed/web/PostCreate/index.tsx @@ -0,0 +1,70 @@ +'use client' + +import { FC, useCallback } from 'react' + +import { setFormRelayErrors, useNotification } from '@baseapp-frontend/utils' + +import { zodResolver } from '@hookform/resolvers/zod' +import { useRouter } from 'next/navigation' +import { useForm } from 'react-hook-form' + +import { useContentPostCreateMutation } from '../../common' +import PostForm from '../PostForm' +import { + CONTENT_POST_CREATE_FORM_VALIDATION, + DEFAULT_CONTENT_POST_CREATE_FORM_VALUES, +} from '../PostForm/constants' +import { ContentPostCreateForm, UploadableContentPostFiles } from '../PostForm/types' + +const PostCreate: FC = () => { + const router = useRouter() + const { sendToast } = useNotification() + const [commitMutation, isMutationInFlight] = useContentPostCreateMutation() + + const form = useForm({ + defaultValues: DEFAULT_CONTENT_POST_CREATE_FORM_VALUES, + resolver: zodResolver(CONTENT_POST_CREATE_FORM_VALIDATION), + mode: 'onBlur', + }) + + const onSubmit = form.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, + isReactionsEnabled: data.isReactionsEnabled, + }, + }, + uploadables, + onCompleted(response) { + const errors = response.contentPostCreate?.errors + if (errors) { + sendToast('Something went wrong', { type: 'error' }) + setFormRelayErrors(form, errors) + } else { + form.reset({ content: '', isReactionsEnabled: true }) + sendToast('Post Created Successfully', { type: 'success' }) + router.push(`/posts/${response.contentPostCreate?.contentPost?.node?.id}`) + } + }, + }) + }) + + const onCancel = useCallback(() => { + router.push('/posts') + }, [router]) + + return ( + + ) +} + +export default PostCreate diff --git a/packages/components/modules/content-feed/web/PostCreate/types.ts b/packages/components/modules/content-feed/web/PostCreate/types.ts new file mode 100644 index 00000000..0a709a31 --- /dev/null +++ b/packages/components/modules/content-feed/web/PostCreate/types.ts @@ -0,0 +1 @@ +export interface PostCreateFormProps {} diff --git a/packages/components/modules/content-feed/web/PostFooter/index.tsx b/packages/components/modules/content-feed/web/PostFooter/index.tsx new file mode 100644 index 00000000..6d5b5799 --- /dev/null +++ b/packages/components/modules/content-feed/web/PostFooter/index.tsx @@ -0,0 +1,35 @@ +'use client' + +import { FC } from 'react' + +import { ReplyIcon, SharePostIcon } from '@baseapp-frontend/design-system/components/web/icons' + +import { Circle as CircleIcon } from '@mui/icons-material' +import { IconButton, Stack, Typography } from '@mui/material' +import { DateTime } from 'luxon' + +import PostReactionButton from '../PostReactionButton' +import { PostFooterProps } from './types' + +const PostFooter: FC = ({ post }) => { + const created = DateTime.fromISO(post.created) + return ( + + + + + + + + + + + + {created.toFormat('hh:mm a')} {' '} + {created.toFormat('DDD')} + + + ) +} + +export default PostFooter diff --git a/packages/components/modules/content-feed/web/PostFooter/types.ts b/packages/components/modules/content-feed/web/PostFooter/types.ts new file mode 100644 index 00000000..70f85bb7 --- /dev/null +++ b/packages/components/modules/content-feed/web/PostFooter/types.ts @@ -0,0 +1,5 @@ +import { ContentPost_post$data } from '../../../../__generated__/ContentPost_post.graphql' + +export interface PostFooterProps { + post: ContentPost_post$data +} diff --git a/packages/components/modules/content-feed/web/PostForm/constants.ts b/packages/components/modules/content-feed/web/PostForm/constants.ts new file mode 100644 index 00000000..1d0f8216 --- /dev/null +++ b/packages/components/modules/content-feed/web/PostForm/constants.ts @@ -0,0 +1,22 @@ +import { z } from 'zod' + +import { ContentPostCreateForm, ContentPostUpdateForm } from './types' + +export const DEFAULT_CONTENT_POST_CREATE_FORM_VALUES = { + content: '', + images: [] as (File | Blob)[], + isReactionsEnabled: true, +} + +export const CONTENT_POST_CREATE_FORM_VALIDATION = z.object({ + content: z.string(), + images: z.array(z.union([z.instanceof(File), z.instanceof(Blob)])).optional(), + isReactionsEnabled: z.boolean(), +} satisfies Record) + +export const CONTENT_POST_UPDATE_FORM_VALIDATION = z.object({ + id: z.string(), + content: z.string(), + images: z.array(z.union([z.instanceof(File), z.instanceof(Blob)])), + isReactionsEnabled: z.boolean(), +} satisfies Record) diff --git a/packages/components/modules/content-feed/web/PostForm/index.tsx b/packages/components/modules/content-feed/web/PostForm/index.tsx new file mode 100644 index 00000000..6982ed14 --- /dev/null +++ b/packages/components/modules/content-feed/web/PostForm/index.tsx @@ -0,0 +1,77 @@ +'use client' + +import { FC } from 'react' + +import { TextField } from '@baseapp-frontend/design-system/components/web/inputs' + +import { LoadingButton } from '@mui/lab' +import { Box, Button, FormControlLabel, Switch, Typography } from '@mui/material' +import { Controller } from 'react-hook-form' + +import PostImageDropzone from '../PostImageDropzone' +import { ButtonContainer, HeaderContainer, RootContainer } from '../styled' +import { PostFormProps } from './types' + +const PostForm: FC = ({ form, onSubmit, onCancel, isSaving }) => { + const isReactionsEnabled = form.watch('isReactionsEnabled') + const { + formState: { isDirty, isValid }, + } = form + + return ( + +
+ + + New Post + + + + + Publish + + + + + + + + ( + + field.onChange(!e.target.checked) + } + /> + } + label="Disable Reactions to this post" + /> + )} + /> + +
+ ) +} + +export default PostForm diff --git a/packages/components/modules/content-feed/web/PostForm/types.ts b/packages/components/modules/content-feed/web/PostForm/types.ts new file mode 100644 index 00000000..8e3203e8 --- /dev/null +++ b/packages/components/modules/content-feed/web/PostForm/types.ts @@ -0,0 +1,25 @@ +import { UseFormReturn } from 'react-hook-form' + +export interface ContentPostCreateForm { + content: string + images?: File[] | Blob[] + isReactionsEnabled: boolean +} + +export interface ContentPostUpdateForm { + id: string + content: string + images?: string[] | File[] | Blob[] + isReactionsEnabled: boolean +} + +export type UploadableContentPostFiles = { + [key: `images.${number}`]: File | Blob +} + +export interface PostFormProps { + form: UseFormReturn + onSubmit: () => void + isSaving: boolean + onCancel: () => void +} diff --git a/packages/components/modules/content-feed/web/PostHeader/index.tsx b/packages/components/modules/content-feed/web/PostHeader/index.tsx new file mode 100644 index 00000000..8d317f1a --- /dev/null +++ b/packages/components/modules/content-feed/web/PostHeader/index.tsx @@ -0,0 +1,37 @@ +'use client' + +import { FC } from 'react' + +import { MoreVert as MoreVertIcon } from '@mui/icons-material' +import { Avatar, IconButton, Stack, Typography } from '@mui/material' +import { useFragment } from 'react-relay' + +import { ProfileItemFragment$key } from '../../../../__generated__/ProfileItemFragment.graphql' +import { ProfileItemFragment } from '../../../profiles/common' +import { PostHeaderProps } from './types' + +const PostHeader: FC = ({ post }) => { + const profile = useFragment(ProfileItemFragment, post?.profile) + + if (!profile) return null + + const { image, name, urlPath } = profile + return ( + + + + + {name} + + {urlPath?.path && <>@{urlPath.path.replace('/', '')}} + + + + + + + + ) +} + +export default PostHeader diff --git a/packages/components/modules/content-feed/web/PostHeader/types.ts b/packages/components/modules/content-feed/web/PostHeader/types.ts new file mode 100644 index 00000000..6f79f4b1 --- /dev/null +++ b/packages/components/modules/content-feed/web/PostHeader/types.ts @@ -0,0 +1,5 @@ +import { ContentPost_post$data } from '../../../../__generated__/ContentPost_post.graphql' + +export interface PostHeaderProps { + post: ContentPost_post$data +} diff --git a/packages/components/modules/content-feed/web/ContentFeedImage/index.tsx b/packages/components/modules/content-feed/web/PostImageDropzone/index.tsx similarity index 86% rename from packages/components/modules/content-feed/web/ContentFeedImage/index.tsx rename to packages/components/modules/content-feed/web/PostImageDropzone/index.tsx index 5c4788d1..70dd701c 100644 --- a/packages/components/modules/content-feed/web/ContentFeedImage/index.tsx +++ b/packages/components/modules/content-feed/web/PostImageDropzone/index.tsx @@ -7,9 +7,9 @@ import { Dropzone } from '@baseapp-frontend/design-system/components/web/dropzon import { Box, Typography } from '@mui/material' import Image from 'next/image' -import { IContentFeedImageProps } from './types' +import { PostImageDropzoneProps } from './types' -const ContentFeedImage: FC = ({ form }) => { +const PostImageDropzone: FC = ({ form }) => { const [selectedPreview, setSelectedPreview] = useState() const [selectedPreviewIndex, setSelectedPreviewIndex] = useState() @@ -19,7 +19,7 @@ const ContentFeedImage: FC = ({ form }) => { const handleRemoveFile = (fileIndex?: number) => { const updatedFiles = formFiles?.filter((_, index) => index !== fileIndex) - form.setValue('images', updatedFiles as File[], { shouldValidate: true }) + form.setValue('images', updatedFiles || [], { shouldValidate: true }) if (selectedPreviewIndex === fileIndex) { setSelectedPreview(undefined) @@ -29,7 +29,7 @@ const ContentFeedImage: FC = ({ form }) => { const onSelect = (files: (File | Blob)[]) => { if (files.length) { - form.setValue('images', [...formFiles, ...(files as File[])]) + form.setValue('images', [...(formFiles || []), ...(files as File[])]) } } @@ -69,4 +69,4 @@ const ContentFeedImage: FC = ({ form }) => { ) } -export default ContentFeedImage +export default PostImageDropzone diff --git a/packages/components/modules/content-feed/web/PostImageDropzone/types.ts b/packages/components/modules/content-feed/web/PostImageDropzone/types.ts new file mode 100644 index 00000000..0eccee9c --- /dev/null +++ b/packages/components/modules/content-feed/web/PostImageDropzone/types.ts @@ -0,0 +1,7 @@ +import { UseFormReturn } from 'react-hook-form' + +import { ContentPostCreateForm } from '../PostForm/types' + +export interface PostImageDropzoneProps { + form: UseFormReturn +} diff --git a/packages/components/modules/content-feed/web/PostImageSlide/index.tsx b/packages/components/modules/content-feed/web/PostImageSlide/index.tsx new file mode 100644 index 00000000..c5fcc4bf --- /dev/null +++ b/packages/components/modules/content-feed/web/PostImageSlide/index.tsx @@ -0,0 +1,19 @@ +'use client' + +import { FC } from 'react' + +import { useFragment } from 'react-relay' + +import { ContentPostImageFragment } from '../../common/graphql/fragments/ContentPostImage' +import { ImageSlide } from './styled' +import { PostImageSlideProps } from './types' + +const PostImageSlide: FC = ({ imagesRef }) => { + const target = useFragment(ContentPostImageFragment, imagesRef) + + if (!target?.image?.url) return null + + return +} + +export default PostImageSlide diff --git a/packages/components/modules/content-feed/web/PostImageSlide/styled.tsx b/packages/components/modules/content-feed/web/PostImageSlide/styled.tsx new file mode 100644 index 00000000..8bb23a27 --- /dev/null +++ b/packages/components/modules/content-feed/web/PostImageSlide/styled.tsx @@ -0,0 +1,6 @@ +import { styled } from '@mui/material' + +export const ImageSlide = styled('img')(() => ({ + height: '100%', + userSelect: 'none', +})) diff --git a/packages/components/modules/content-feed/web/PostImageSlide/types.tsx b/packages/components/modules/content-feed/web/PostImageSlide/types.tsx new file mode 100644 index 00000000..fecae810 --- /dev/null +++ b/packages/components/modules/content-feed/web/PostImageSlide/types.tsx @@ -0,0 +1,5 @@ +import { ContentPostImageFragment$key } from '../../../../__generated__/ContentPostImageFragment.graphql' + +export interface PostImageSlideProps { + imagesRef: ContentPostImageFragment$key +} diff --git a/packages/components/modules/content-feed/web/PostItem/index.tsx b/packages/components/modules/content-feed/web/PostItem/index.tsx new file mode 100644 index 00000000..a58a7976 --- /dev/null +++ b/packages/components/modules/content-feed/web/PostItem/index.tsx @@ -0,0 +1,35 @@ +'use client' + +import { FC } from 'react' + +import { Stack, Typography } from '@mui/material' +import { useRefetchableFragment } from 'react-relay' + +import { ContentPostRefetchQuery } from '../../../../__generated__/ContentPostRefetchQuery.graphql' +import { ContentPost_post$key } from '../../../../__generated__/ContentPost_post.graphql' +import { ContentPostFragmentQuery } from '../../common/graphql/fragments/ContentPost' +import PostFooter from '../PostFooter' +import PostHeader from '../PostHeader' +import PostItemImages from '../PostItemImages' +import { PostItemProps } from './types' + +const PostItem: FC = ({ postRef }) => { + const [post] = useRefetchableFragment( + ContentPostFragmentQuery, + postRef, + ) + if (!post) return null + + return ( + + + + + {post.content} + + + + ) +} + +export default PostItem diff --git a/packages/components/modules/content-feed/web/PostItem/types.ts b/packages/components/modules/content-feed/web/PostItem/types.ts new file mode 100644 index 00000000..7eb90bb2 --- /dev/null +++ b/packages/components/modules/content-feed/web/PostItem/types.ts @@ -0,0 +1,5 @@ +import { ContentPost_post$key } from '../../../../__generated__/ContentPost_post.graphql' + +export interface PostItemProps { + postRef: ContentPost_post$key +} diff --git a/packages/components/modules/content-feed/web/PostItemImages/index.tsx b/packages/components/modules/content-feed/web/PostItemImages/index.tsx new file mode 100644 index 00000000..06585f54 --- /dev/null +++ b/packages/components/modules/content-feed/web/PostItemImages/index.tsx @@ -0,0 +1,128 @@ +'use client' + +import { FC, MouseEvent } from 'react' + +import { PillIcon } from '@baseapp-frontend/design-system/components/web/icons' + +import { + ChevronLeft as ChevronLeftIcon, + ChevronRight as ChevronRightIcon, + Circle as CircleIcon, +} from '@mui/icons-material' +import { Fab, IconButton } from '@mui/material' +import RMCarousel, { ArrowProps, DotProps } from 'react-multi-carousel' +import 'react-multi-carousel/lib/styles.css' + +import PostImageSlide from '../PostImageSlide' +import { ImageCarouselContainer } from './styled' +import { PostItemImagesProps } from './types' + +// @ts-ignore +const Carousel = RMCarousel.default ? RMCarousel.default : RMCarousel + +const CustomDot: FC = ({ onClick, active }) => ( + ) => { + onClick?.() + e.preventDefault() + }} + > + {active ? ( + + ) : ( + + )} + +) + +const CustomArrow: FC = ({ + onClick, + orientation, +}) => ( + + {orientation === 'left' ? : } + +) + +const PostItemImages: FC = ({ post }) => { + if (!post?.images?.edges?.length) return null + + const images = post?.images?.edges.filter((img) => !!img?.node) || [] + + return ( + 1} + className="" + containerClass="content-feed-swiper" + customLeftArrow={} + customRightArrow={} + customDot={} + dotListClass="content-feed-swiper-custom-dot" + draggable={images.length > 1} + focusOnSelect={false} + itemClass="content-feed-swiper-item" + keyBoardControl + minimumTouchDrag={80} + renderArrowsWhenDisabled={false} + infinite + renderDotsOutside={false} + responsive={{ + desktop: { + breakpoint: { max: 3000, min: 1024 }, + items: 1, + partialVisibilityGutter: 0, + }, + mobile: { + breakpoint: { + max: 464, + min: 0, + }, + items: 1, + partialVisibilityGutter: 0, + }, + tablet: { + breakpoint: { + max: 1024, + min: 464, + }, + items: 1, + partialVisibilityGutter: 0, + }, + }} + rewind={false} + rewindWithAnimation={false} + rtl={false} + shouldResetAutoplay + showDots={post.images.edges.length > 1} + sliderClass="" + slidesToSlide={1} + swipeable={post.images.edges.length > 1} + > + {images.map( + (image) => + image?.node && ( + + + + ), + )} + + ) +} + +export default PostItemImages diff --git a/packages/components/modules/content-feed/web/PostItemImages/styled.tsx b/packages/components/modules/content-feed/web/PostItemImages/styled.tsx new file mode 100644 index 00000000..8b226c34 --- /dev/null +++ b/packages/components/modules/content-feed/web/PostItemImages/styled.tsx @@ -0,0 +1,9 @@ +import { Box, styled } from '@mui/material' + +export const ImageCarouselContainer = styled(Box)(() => ({ + background: '#000', + width: 600, + height: '100%', + justifyContent: 'center', + display: 'flex', +})) diff --git a/packages/components/modules/content-feed/web/PostItemImages/types.ts b/packages/components/modules/content-feed/web/PostItemImages/types.ts new file mode 100644 index 00000000..2252a3cb --- /dev/null +++ b/packages/components/modules/content-feed/web/PostItemImages/types.ts @@ -0,0 +1,5 @@ +import { ContentPost_post$data } from '../../../../__generated__/ContentPost_post.graphql' + +export interface PostItemImagesProps { + post: ContentPost_post$data +} diff --git a/packages/components/modules/content-feed/web/PostList/index.tsx b/packages/components/modules/content-feed/web/PostList/index.tsx new file mode 100644 index 00000000..6be7b60b --- /dev/null +++ b/packages/components/modules/content-feed/web/PostList/index.tsx @@ -0,0 +1,72 @@ +'use client' + +import React, { FC } from 'react' + +import { LoadingState } from '@baseapp-frontend/design-system/components/web/displays' + +import { Box } from '@mui/material' +import { Components, Virtuoso } from 'react-virtuoso' + +import { useContentPosts } from '../../common' +import { ContentFeedProps, ContentPostEdges } from '../ContentFeed/types' +import PostItem from '../PostItem' +import { PostsListContainer } from './styled' + +const Scroller: Components['List'] = React.forwardRef(({ style, children }, ref) => ( + + {children} + +)) + +const PostList: FC = ({ preloadedQuery }) => { + const { + data: { contentPosts }, + loadNext, + isLoadingNext, + hasNext, + } = useContentPosts(preloadedQuery) + + const renderPostItem = (post: ContentPostEdges[number]) => { + if (!post?.node) return null + return + } + + const renderLoadingState = () => { + if (!isLoadingNext) return + + return ( + + ) + } + + const renderHeader = () => { + if (contentPosts?.edges?.length === 0) return null + + return
+ } + + return ( + renderPostItem(post)} + components={{ + Header: renderHeader, + Footer: renderLoadingState, + List: Scroller, + }} + endReached={() => { + if (hasNext) { + loadNext(5) + } + }} + /> + ) +} + +export default PostList diff --git a/packages/components/modules/content-feed/web/PostList/styled.tsx b/packages/components/modules/content-feed/web/PostList/styled.tsx new file mode 100644 index 00000000..c5d4b368 --- /dev/null +++ b/packages/components/modules/content-feed/web/PostList/styled.tsx @@ -0,0 +1,8 @@ +import { Stack, styled } from '@mui/material' + +export const PostsListContainer = styled(Stack)(({ theme }) => ({ + '& > :not(:last-of-type)': { + borderBottom: '1px solid', + borderColor: theme.palette.divider, + }, +})) diff --git a/packages/components/modules/content-feed/web/PostReactionButton/index.tsx b/packages/components/modules/content-feed/web/PostReactionButton/index.tsx new file mode 100644 index 00000000..b855cb91 --- /dev/null +++ b/packages/components/modules/content-feed/web/PostReactionButton/index.tsx @@ -0,0 +1,37 @@ +import { FC } from 'react' + +import { IconButton } from '@baseapp-frontend/design-system/components/web/buttons' +import { + FavoriteIcon, + FavoriteSelectedIcon, +} from '@baseapp-frontend/design-system/components/web/icons' + +import { Typography } from '@mui/material' + +import { ReactionButton } from '../../../__shared__/web' +import { PostReactionButtonProps } from './types' + +const PostReactionButton: FC = ({ target: targetRef }) => ( + + {({ handleReaction, target }) => ( +
+ + {target?.myReaction?.id ? ( + + ) : ( + + )} + + + {target?.reactionsCount?.total} + +
+ )} +
+) + +export default PostReactionButton diff --git a/packages/components/modules/content-feed/web/PostReactionButton/types.ts b/packages/components/modules/content-feed/web/PostReactionButton/types.ts new file mode 100644 index 00000000..2017a67a --- /dev/null +++ b/packages/components/modules/content-feed/web/PostReactionButton/types.ts @@ -0,0 +1,5 @@ +import { ReactionButton_target$key } from '../../../../__generated__/ReactionButton_target.graphql' + +export interface PostReactionButtonProps { + target: ReactionButton_target$key +} diff --git a/packages/components/modules/content-feed/web/index.ts b/packages/components/modules/content-feed/web/index.ts index 8cace7dc..5474fbc7 100644 --- a/packages/components/modules/content-feed/web/index.ts +++ b/packages/components/modules/content-feed/web/index.ts @@ -3,8 +3,28 @@ 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 PostImageDropzone } from './PostImageDropzone' +export type * from './PostImageDropzone/types' -export { default as NewContentPost } from './NewContentPost' -export type * from './NewContentPost/types' +export { default as PostCreate } from './PostCreate' +export type * from './PostCreate/types' + +export { default as PostItem } from './PostItem' +export type * from './PostItem/types' + +export { default as PostHeader } from './PostHeader' +export type * from './PostHeader/types' + +export { default as PostFooter } from './PostFooter' +export type * from './PostFooter/types' + +export { default as PostItemImages } from './PostItemImages' +export type * from './PostItemImages/types' + +export { default as PostImageSlide } from './PostImageSlide' +export type * from './PostImageSlide/types' + +export { default as PostList } from './PostList' + +export { default as PostReactionButton } from './PostReactionButton' +export type * from './PostReactionButton/types' diff --git a/packages/components/modules/content-feed/web/ContentFeed/styled.tsx b/packages/components/modules/content-feed/web/styled.tsx similarity index 100% rename from packages/components/modules/content-feed/web/ContentFeed/styled.tsx rename to packages/components/modules/content-feed/web/styled.tsx diff --git a/packages/components/package.json b/packages/components/package.json index 96cd4aab..2480f06d 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -1,7 +1,7 @@ { "name": "@baseapp-frontend/components", "description": "BaseApp components modules such as comments, notifications, messages, and more.", - "version": "1.2.0", + "version": "1.2.1", "sideEffects": false, "scripts": { "babel:transpile": "babel modules -d tmp-babel --extensions .ts,.tsx --ignore '**/__tests__/**','**/__storybook__/**'", @@ -107,6 +107,7 @@ "slugify": "^1.6.6", "use-long-press": "^3.2.0", "zod": "catalog:", + "react-multi-carousel": "catalog:", "zustand": "catalog:" }, "peerDependencies": { diff --git a/packages/components/tsup.config.ts b/packages/components/tsup.config.ts index 5f6d40c2..560e5742 100644 --- a/packages/components/tsup.config.ts +++ b/packages/components/tsup.config.ts @@ -3,7 +3,7 @@ /* eslint-disable no-param-reassign */ import { defineConfig } from 'tsup' -export default defineConfig((options) => ({ +export default defineConfig(() => ({ clean: true, dts: false, entry: ['./modules/**/(common|web|native)/index.ts'], @@ -12,6 +12,7 @@ export default defineConfig((options) => ({ ...esbuildOptions.loader, '.css': 'file', } + esbuildOptions.external = ['react-multi-carousel', 'react-multi-carousel/lib/styles.css'] }, external: [ 'react', @@ -23,9 +24,11 @@ export default defineConfig((options) => ({ '@emotion/*', 'zod', 'zustand', + 'react-multi-carousel', + 'react-multi-carousel/lib/styles.css', ], format: ['esm', 'cjs'], - minify: !options.watch, + minify: false, outDir: 'dist', sourcemap: true, splitting: true, diff --git a/packages/design-system/CHANGELOG.md b/packages/design-system/CHANGELOG.md index ed418d6e..b5d684c8 100644 --- a/packages/design-system/CHANGELOG.md +++ b/packages/design-system/CHANGELOG.md @@ -1,5 +1,11 @@ # @baseapp-frontend/design-system +## 1.0.19 + +### Patch Changes + +- Content Feed Posts list with images and reactions + ## 1.0.18 ### Patch Changes diff --git a/packages/design-system/components/web/icons/PillIcon/index.tsx b/packages/design-system/components/web/icons/PillIcon/index.tsx new file mode 100644 index 00000000..7d7518ac --- /dev/null +++ b/packages/design-system/components/web/icons/PillIcon/index.tsx @@ -0,0 +1,11 @@ +import { FC } from 'react' + +import { SvgIcon, SvgIconProps } from '@mui/material' + +const PillIcon: FC = ({ sx, ...props }) => ( + + + +) + +export default PillIcon diff --git a/packages/design-system/components/web/icons/ReplyIcon/index.tsx b/packages/design-system/components/web/icons/ReplyIcon/index.tsx new file mode 100644 index 00000000..1a2c0996 --- /dev/null +++ b/packages/design-system/components/web/icons/ReplyIcon/index.tsx @@ -0,0 +1,22 @@ +import { FC } from 'react' + +import { SvgIcon, SvgIconProps } from '@mui/material' + +const ReplyIcon: FC = ({ sx, ...props }) => ( + + + + + +) + +export default ReplyIcon diff --git a/packages/design-system/components/web/icons/SharePostIcon/index.tsx b/packages/design-system/components/web/icons/SharePostIcon/index.tsx new file mode 100644 index 00000000..0809b0ea --- /dev/null +++ b/packages/design-system/components/web/icons/SharePostIcon/index.tsx @@ -0,0 +1,14 @@ +import { FC } from 'react' + +import { SvgIcon, SvgIconProps } from '@mui/material' + +const SharePostIcon: FC = ({ sx, ...props }) => ( + + + +) + +export default SharePostIcon diff --git a/packages/design-system/components/web/icons/index.ts b/packages/design-system/components/web/icons/index.ts index 2a0e37ac..260b93c4 100644 --- a/packages/design-system/components/web/icons/index.ts +++ b/packages/design-system/components/web/icons/index.ts @@ -35,3 +35,6 @@ export { default as UnarchiveIcon } from './UnarchiveIcon' export { default as UnblockIcon } from './UnblockIcon' export { default as UnreadIcon } from './UnreadIcon' export { default as UsernameIcon } from './UsernameIcon' +export { default as PillIcon } from './PillIcon' +export { default as ReplyIcon } from './ReplyIcon' +export { default as SharePostIcon } from './SharePostIcon' diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d800c0dd..925a88f6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -48,6 +48,9 @@ catalogs: react-international-phone: specifier: 4.5.0 version: 4.5.0 + react-multi-carousel: + specifier: 2.8.6 + version: 2.8.6 react-virtuoso: specifier: 4.12.7 version: 4.12.7 @@ -625,6 +628,9 @@ importers: react-hook-form: specifier: 'catalog:' version: 7.56.4(react@19.1.0) + react-multi-carousel: + specifier: 'catalog:' + version: 2.8.6 react-native: specifier: catalog:react-native version: 0.79.2(@babel/core@7.27.4)(@types/react@19.1.4)(react@19.1.0) @@ -5961,6 +5967,9 @@ packages: core-js-compat@3.43.0: resolution: {integrity: sha512-2GML2ZsCc5LR7hZYz4AXmjQw8zuy2T//2QntwdnpuYI7jteT6GVYJL7F6C2C57R7gSYrcqVW3lAALefdbhBLDA==} + core-js@3.43.0: + resolution: {integrity: sha512-N6wEbTTZSYOY2rYAn85CuvWWkCK6QweMn7/4Nr3w+gDBeBhk/x4EJeY6FPo4QzDoJZxVTv8U7CMvgWk6pOHHqA==} + core-util-is@1.0.2: resolution: {integrity: sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==} @@ -7448,6 +7457,10 @@ packages: resolution: {integrity: sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA==} engines: {node: '>=10'} + install@0.13.0: + resolution: {integrity: sha512-zDml/jzr2PKU9I8J/xyZBQn8rPCAY//UOYNmR01XwNwyfhEWObo2SWfSl1+0tm1u6PhxLwDnfsT/6jB7OUxqFA==} + engines: {node: '>= 0.10'} + internal-slot@1.1.0: resolution: {integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==} engines: {node: '>= 0.4'} @@ -8692,6 +8705,80 @@ packages: resolution: {integrity: sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA==} engines: {node: '>=18'} + npm@10.9.3: + resolution: {integrity: sha512-6Eh1u5Q+kIVXeA8e7l2c/HpnFFcwrkt37xDMujD5be1gloWa9p6j3Fsv3mByXXmqJHy+2cElRMML8opNT7xIJQ==} + engines: {node: ^18.17.0 || >=20.5.0} + hasBin: true + bundledDependencies: + - '@isaacs/string-locale-compare' + - '@npmcli/arborist' + - '@npmcli/config' + - '@npmcli/fs' + - '@npmcli/map-workspaces' + - '@npmcli/package-json' + - '@npmcli/promise-spawn' + - '@npmcli/redact' + - '@npmcli/run-script' + - '@sigstore/tuf' + - abbrev + - archy + - cacache + - chalk + - ci-info + - cli-columns + - fastest-levenshtein + - fs-minipass + - glob + - graceful-fs + - hosted-git-info + - ini + - init-package-json + - is-cidr + - json-parse-even-better-errors + - libnpmaccess + - libnpmdiff + - libnpmexec + - libnpmfund + - libnpmhook + - libnpmorg + - libnpmpack + - libnpmpublish + - libnpmsearch + - libnpmteam + - libnpmversion + - make-fetch-happen + - minimatch + - minipass + - minipass-pipeline + - ms + - node-gyp + - nopt + - normalize-package-data + - npm-audit-report + - npm-install-checks + - npm-package-arg + - npm-pick-manifest + - npm-profile + - npm-registry-fetch + - npm-user-validate + - p-map + - pacote + - parse-conflict-json + - proc-log + - qrcode-terminal + - read + - semver + - spdx-expression-parse + - ssri + - supports-color + - tar + - text-table + - tiny-relative-date + - treeverse + - validate-npm-package-name + - which + - write-file-atomic + nth-check@2.1.1: resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==} @@ -9398,6 +9485,10 @@ packages: peerDependencies: react: ^15.x.x || ^16.x.x || ^17.x.x || ^18.x.x || ^19.x.x + react-multi-carousel@2.8.6: + resolution: {integrity: sha512-sAX5I9Xec3MR9FxLgZYcYqNnY8M8zJxRRwRipMPtuh4BGvcvoptJfX8Z6nRn0ROMkqVO72iAmb83GlqmSW4Gqw==} + engines: {node: '>=8'} + react-native-drawer-layout@4.1.11: resolution: {integrity: sha512-31gilubSKPLToy31/bb0hhgOOenHYJq4JC7g/JkIEqBqSWzoCgiOlccDHlBRG+MV37UtXZnJN2spj3VusdCd4A==} peerDependencies: @@ -16741,6 +16832,8 @@ snapshots: dependencies: browserslist: 4.25.0 + core-js@3.43.0: {} + core-util-is@1.0.2: {} core-util-is@1.0.3: {} @@ -18728,6 +18821,8 @@ snapshots: ini@2.0.0: {} + install@0.13.0: {} + internal-slot@1.1.0: dependencies: es-errors: 1.3.0 @@ -20287,6 +20382,8 @@ snapshots: path-key: 4.0.0 unicorn-magic: 0.3.0 + npm@10.9.3: {} + nth-check@2.1.1: dependencies: boolbase: 1.0.0 @@ -20953,6 +21050,12 @@ snapshots: lodash.throttle: 4.1.1 react: 19.1.0 + react-multi-carousel@2.8.6: + dependencies: + core-js: 3.43.0 + install: 0.13.0 + npm: 10.9.3 + react-native-drawer-layout@4.1.11(react-native-gesture-handler@2.24.0(react-native@0.79.2(@babel/core@7.27.4)(@types/react@19.1.4)(react@19.1.0))(react@19.1.0))(react-native-reanimated@3.17.5(@babel/core@7.27.4)(react-native@0.79.2(@babel/core@7.27.4)(@types/react@19.1.4)(react@19.1.0))(react@19.1.0))(react-native@0.79.2(@babel/core@7.27.4)(@types/react@19.1.4)(react@19.1.0))(react@19.1.0): dependencies: react: 19.1.0 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index a6ac6b44..4c18b9c4 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -24,6 +24,7 @@ catalog: typescript: 5.8.3 zod: 3.25.7 zustand: 5.0.4 + react-multi-carousel: 2.8.6 catalogs: react19: