diff --git a/app/soapbox/components/ui/column/column.tsx b/app/soapbox/components/ui/column/column.tsx index 317cf9841f..d6cadec775 100644 --- a/app/soapbox/components/ui/column/column.tsx +++ b/app/soapbox/components/ui/column/column.tsx @@ -7,10 +7,10 @@ import { useSoapboxConfig } from 'soapbox/hooks'; import { Card, CardBody, CardHeader, CardTitle } from '../card/card'; -type IColumnHeader = Pick; +type IColumnHeader = Pick; /** Contains the column title with optional back button. */ -const ColumnHeader: React.FC = ({ label, backHref, className }) => { +const ColumnHeader: React.FC = ({ label, backHref, className, action }) => { const history = useHistory(); const handleBackClick = () => { @@ -29,6 +29,12 @@ const ColumnHeader: React.FC = ({ label, backHref, className }) = return ( + + {action && ( +
+ {action} +
+ )}
); }; @@ -48,11 +54,12 @@ export interface IColumn { ref?: React.Ref /** Children to display in the column. */ children?: React.ReactNode + action?: React.ReactNode } /** A backdrop for the main section of the UI. */ const Column: React.FC = React.forwardRef((props, ref: React.ForwardedRef): JSX.Element => { - const { backHref, children, label, transparent = false, withHeader = true, className } = props; + const { backHref, children, label, transparent = false, withHeader = true, className, action } = props; const soapboxConfig = useSoapboxConfig(); return ( @@ -75,6 +82,7 @@ const Column: React.FC = React.forwardRef((props, ref: React.ForwardedR label={label} backHref={backHref} className={clsx({ 'px-4 pt-4 sm:p-0': transparent })} + action={action} /> )} diff --git a/app/soapbox/features/groups/components/discover/popular-groups.tsx b/app/soapbox/features/groups/components/discover/popular-groups.tsx index 79024466b1..c5bf90307c 100644 --- a/app/soapbox/features/groups/components/discover/popular-groups.tsx +++ b/app/soapbox/features/groups/components/discover/popular-groups.tsx @@ -1,7 +1,8 @@ import React, { useState } from 'react'; import { FormattedMessage } from 'react-intl'; -import { Carousel, Stack, Text } from 'soapbox/components/ui'; +import Link from 'soapbox/components/link'; +import { Carousel, HStack, Stack, Text } from 'soapbox/components/ui'; import PlaceholderGroupDiscover from 'soapbox/features/placeholder/components/placeholder-group-discover'; import { usePopularGroups } from 'soapbox/hooks/api/usePopularGroups'; @@ -15,12 +16,23 @@ const PopularGroups = () => { return ( - - - + + + + + + + + + + + {isEmpty ? ( diff --git a/app/soapbox/features/groups/components/discover/suggested-groups.tsx b/app/soapbox/features/groups/components/discover/suggested-groups.tsx index 20686a7cc4..9925a423ea 100644 --- a/app/soapbox/features/groups/components/discover/suggested-groups.tsx +++ b/app/soapbox/features/groups/components/discover/suggested-groups.tsx @@ -1,7 +1,8 @@ import React, { useState } from 'react'; import { FormattedMessage } from 'react-intl'; -import { Carousel, Stack, Text } from 'soapbox/components/ui'; +import Link from 'soapbox/components/link'; +import { Carousel, HStack, Stack, Text } from 'soapbox/components/ui'; import PlaceholderGroupDiscover from 'soapbox/features/placeholder/components/placeholder-group-discover'; import { useSuggestedGroups } from 'soapbox/hooks/api/useSuggestedGroups'; @@ -15,12 +16,23 @@ const SuggestedGroups = () => { return ( - - - + + + + + + + + + + + {isEmpty ? ( diff --git a/app/soapbox/features/groups/index.tsx b/app/soapbox/features/groups/index.tsx index 30319160b8..c84ddbd53d 100644 --- a/app/soapbox/features/groups/index.tsx +++ b/app/soapbox/features/groups/index.tsx @@ -60,9 +60,13 @@ const Groups: React.FC = () => { return ( + {features.groupsDiscovery && ( + + )} + {canCreateGroup && ( )} - {features.groupsDiscovery && ( - - )} - { + const { context, ...rest } = props; + return
; +}); + + +const Popular: React.FC = () => { + const intl = useIntl(); + + const [layout, setLayout] = useState(Layout.LIST); + + const { groups, hasNextPage, fetchNextPage } = usePopularGroups(); + + const handleLoadMore = () => { + if (hasNextPage) { + fetchNextPage(); + } + }; + + const renderGroupList = useCallback((group: Group, index: number) => ( +
+ +
+ ), []); + + const renderGroupGrid = useCallback((group: Group, index: number) => ( +
+ +
+ ), []); + + return ( + + + + + + } + > + {layout === Layout.LIST ? ( + renderGroupList(group, index)} + endReached={handleLoadMore} + /> + ) : ( + renderGroupGrid(group, index)} + components={{ + Item: (props) => ( +
+ ), + List: GridList, + }} + endReached={handleLoadMore} + /> + )} + + ); +}; + +export default Popular; diff --git a/app/soapbox/features/groups/suggested.tsx b/app/soapbox/features/groups/suggested.tsx new file mode 100644 index 0000000000..8c17fc0af0 --- /dev/null +++ b/app/soapbox/features/groups/suggested.tsx @@ -0,0 +1,114 @@ +import clsx from 'clsx'; +import React, { useCallback, useState } from 'react'; +import { defineMessages, useIntl } from 'react-intl'; +import { Components, Virtuoso, VirtuosoGrid } from 'react-virtuoso'; + +import { Column, HStack, Icon } from 'soapbox/components/ui'; +import { useSuggestedGroups } from 'soapbox/hooks/api/useSuggestedGroups'; + +import GroupGridItem from './components/discover/group-grid-item'; +import GroupListItem from './components/discover/group-list-item'; + +import type { Group } from 'soapbox/schemas'; + +const messages = defineMessages({ + label: { id: 'groups.popular.label', defaultMessage: 'Suggested Groups' }, +}); + +enum Layout { + LIST = 'LIST', + GRID = 'GRID' +} + +const GridList: Components['List'] = React.forwardRef((props, ref) => { + const { context, ...rest } = props; + return
; +}); + + +const Suggested: React.FC = () => { + const intl = useIntl(); + + const [layout, setLayout] = useState(Layout.LIST); + + const { groups, hasNextPage, fetchNextPage } = useSuggestedGroups(); + + const handleLoadMore = () => { + if (hasNextPage) { + fetchNextPage(); + } + }; + + const renderGroupList = useCallback((group: Group, index: number) => ( +
+ +
+ ), []); + + const renderGroupGrid = useCallback((group: Group, index: number) => ( +
+ +
+ ), []); + + return ( + + + + + + } + > + {layout === Layout.LIST ? ( + renderGroupList(group, index)} + endReached={handleLoadMore} + /> + ) : ( + renderGroupGrid(group, index)} + components={{ + Item: (props) => ( +
+ ), + List: GridList, + }} + endReached={handleLoadMore} + /> + )} + + ); +}; + +export default Suggested; diff --git a/app/soapbox/features/ui/index.tsx b/app/soapbox/features/ui/index.tsx index 38dc88581f..89a156be44 100644 --- a/app/soapbox/features/ui/index.tsx +++ b/app/soapbox/features/ui/index.tsx @@ -118,6 +118,8 @@ import { Events, Groups, GroupsDiscover, + GroupsPopular, + GroupsSuggested, PendingGroupRequests, GroupMembers, GroupTimeline, @@ -289,6 +291,8 @@ const SwitchingColumnsArea: React.FC = ({ children }) => {features.groups && } {features.groupsDiscovery && } + {features.groupsDiscovery && } + {features.groupsDiscovery && } {features.groupsPending && } {features.groups && } {features.groups && } diff --git a/app/soapbox/features/ui/util/async-components.ts b/app/soapbox/features/ui/util/async-components.ts index ec83848844..e9b015d8cb 100644 --- a/app/soapbox/features/ui/util/async-components.ts +++ b/app/soapbox/features/ui/util/async-components.ts @@ -550,6 +550,14 @@ export function GroupsDiscover() { return import(/* webpackChunkName: "features/groups/discover" */'../../groups/discover'); } +export function GroupsPopular() { + return import(/* webpackChunkName: "features/groups/discover" */'../../groups/popular'); +} + +export function GroupsSuggested() { + return import(/* webpackChunkName: "features/groups/discover" */'../../groups/suggested'); +} + export function PendingGroupRequests() { return import(/* webpackChunkName: "features/groups/discover" */'../../groups/pending-requests'); } diff --git a/app/soapbox/locales/en.json b/app/soapbox/locales/en.json index e354e84229..9bccb68a2d 100644 --- a/app/soapbox/locales/en.json +++ b/app/soapbox/locales/en.json @@ -800,6 +800,7 @@ "group.tabs.members": "Members", "group.upload_banner": "Upload photo", "groups.discover.popular.empty": "Unable to fetch popular groups at this time. Please check back later.", + "groups.discover.popular.show_more": "Show More", "groups.discover.popular.title": "Popular Groups", "groups.discover.search.error.subtitle": "Please try again later.", "groups.discover.search.error.title": "An error occurred", @@ -813,6 +814,7 @@ "groups.discover.search.results.groups": "Groups", "groups.discover.search.results.member_count": "{members, plural, one {member} other {members}}", "groups.discover.suggested.empty": "Unable to fetch suggested groups at this time. Please check back later.", + "groups.discover.suggested.show_more": "Show More", "groups.discover.suggested.title": "Suggested For You", "groups.empty.subtitle": "Start discovering groups to join or create your own.", "groups.empty.title": "No Groups yet", @@ -820,6 +822,7 @@ "groups.pending.empty.subtitle": "You have no pending requests at this time.", "groups.pending.empty.title": "No pending requests", "groups.pending.label": "Pending Requests", + "groups.popular.label": "Suggested Groups", "hashtag.column_header.tag_mode.all": "and {additional}", "hashtag.column_header.tag_mode.any": "or {additional}", "hashtag.column_header.tag_mode.none": "without {additional}",