Add pages for Popular / Suggested Groups

develop^2
Chewbacca 2023-03-14 12:46:16 -04:00
rodzic 1b542c3ed7
commit 821b90c372
9 zmienionych plików z 297 dodań i 22 usunięć

Wyświetl plik

@ -7,10 +7,10 @@ import { useSoapboxConfig } from 'soapbox/hooks';
import { Card, CardBody, CardHeader, CardTitle } from '../card/card';
type IColumnHeader = Pick<IColumn, 'label' | 'backHref' |'className'>;
type IColumnHeader = Pick<IColumn, 'label' | 'backHref' | 'className' | 'action'>;
/** Contains the column title with optional back button. */
const ColumnHeader: React.FC<IColumnHeader> = ({ label, backHref, className }) => {
const ColumnHeader: React.FC<IColumnHeader> = ({ label, backHref, className, action }) => {
const history = useHistory();
const handleBackClick = () => {
@ -29,6 +29,12 @@ const ColumnHeader: React.FC<IColumnHeader> = ({ label, backHref, className }) =
return (
<CardHeader className={className} onBackClick={handleBackClick}>
<CardTitle title={label} />
{action && (
<div className='flex grow justify-end'>
{action}
</div>
)}
</CardHeader>
);
};
@ -48,11 +54,12 @@ export interface IColumn {
ref?: React.Ref<HTMLDivElement>
/** 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<IColumn> = React.forwardRef((props, ref: React.ForwardedRef<HTMLDivElement>): 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<IColumn> = React.forwardRef((props, ref: React.ForwardedR
label={label}
backHref={backHref}
className={clsx({ 'px-4 pt-4 sm:p-0': transparent })}
action={action}
/>
)}

Wyświetl plik

@ -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 (
<Stack space={4}>
<Text size='xl' weight='bold'>
<FormattedMessage
id='groups.discover.popular.title'
defaultMessage='Popular Groups'
/>
</Text>
<HStack alignItems='center' justifyContent='between'>
<Text size='xl' weight='bold'>
<FormattedMessage
id='groups.discover.popular.title'
defaultMessage='Popular Groups'
/>
</Text>
<Link to='/groups/popular'>
<Text tag='span' weight='medium' size='sm' theme='inherit'>
<FormattedMessage
id='groups.discover.popular.show_more'
defaultMessage='Show More'
/>
</Text>
</Link>
</HStack>
{isEmpty ? (
<Text theme='muted'>

Wyświetl plik

@ -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 (
<Stack space={4}>
<Text size='xl' weight='bold'>
<FormattedMessage
id='groups.discover.suggested.title'
defaultMessage='Suggested For You'
/>
</Text>
<HStack alignItems='center' justifyContent='between'>
<Text size='xl' weight='bold'>
<FormattedMessage
id='groups.discover.suggested.title'
defaultMessage='Suggested For You'
/>
</Text>
<Link to='/groups/suggested'>
<Text tag='span' weight='medium' size='sm' theme='inherit'>
<FormattedMessage
id='groups.discover.suggested.show_more'
defaultMessage='Show More'
/>
</Text>
</Link>
</HStack>
{isEmpty ? (
<Text theme='muted'>

Wyświetl plik

@ -60,9 +60,13 @@ const Groups: React.FC = () => {
return (
<Stack space={4}>
{features.groupsDiscovery && (
<TabBar activeTab={TabItems.MY_GROUPS} />
)}
{canCreateGroup && (
<Button
className='sm:w-fit sm:self-end xl:hidden'
className='xl:hidden'
icon={require('@tabler/icons/circles.svg')}
onClick={createGroup}
theme='secondary'
@ -72,10 +76,6 @@ const Groups: React.FC = () => {
</Button>
)}
{features.groupsDiscovery && (
<TabBar activeTab={TabItems.MY_GROUPS} />
)}
<PendingGroupsRow />
<ScrollableList

Wyświetl plik

@ -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 { usePopularGroups } from 'soapbox/hooks/api/usePopularGroups';
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: 'Popular Groups' },
});
enum Layout {
LIST = 'LIST',
GRID = 'GRID'
}
const GridList: Components['List'] = React.forwardRef((props, ref) => {
const { context, ...rest } = props;
return <div ref={ref} {...rest} className='flex flex-wrap' />;
});
const Popular: React.FC = () => {
const intl = useIntl();
const [layout, setLayout] = useState<Layout>(Layout.LIST);
const { groups, hasNextPage, fetchNextPage } = usePopularGroups();
const handleLoadMore = () => {
if (hasNextPage) {
fetchNextPage();
}
};
const renderGroupList = useCallback((group: Group, index: number) => (
<div
className={
clsx({
'pt-4': index !== 0,
})
}
>
<GroupListItem group={group} withJoinAction />
</div>
), []);
const renderGroupGrid = useCallback((group: Group, index: number) => (
<div className='pb-4'>
<GroupGridItem group={group} />
</div>
), []);
return (
<Column
label={intl.formatMessage(messages.label)}
action={
<HStack alignItems='center'>
<button onClick={() => setLayout(Layout.LIST)}>
<Icon
src={require('@tabler/icons/layout-list.svg')}
className={
clsx('h-5 w-5 text-gray-600', {
'text-primary-600': layout === Layout.LIST,
})
}
/>
</button>
<button onClick={() => setLayout(Layout.GRID)}>
<Icon
src={require('@tabler/icons/layout-grid.svg')}
className={
clsx('h-5 w-5 text-gray-600', {
'text-primary-600': layout === Layout.GRID,
})
}
/>
</button>
</HStack>
}
>
{layout === Layout.LIST ? (
<Virtuoso
useWindowScroll
data={groups}
itemContent={(index, group) => renderGroupList(group, index)}
endReached={handleLoadMore}
/>
) : (
<VirtuosoGrid
useWindowScroll
data={groups}
itemContent={(index, group) => renderGroupGrid(group, index)}
components={{
Item: (props) => (
<div {...props} className='w-1/2 flex-none' />
),
List: GridList,
}}
endReached={handleLoadMore}
/>
)}
</Column>
);
};
export default Popular;

Wyświetl plik

@ -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 <div ref={ref} {...rest} className='flex flex-wrap' />;
});
const Suggested: React.FC = () => {
const intl = useIntl();
const [layout, setLayout] = useState<Layout>(Layout.LIST);
const { groups, hasNextPage, fetchNextPage } = useSuggestedGroups();
const handleLoadMore = () => {
if (hasNextPage) {
fetchNextPage();
}
};
const renderGroupList = useCallback((group: Group, index: number) => (
<div
className={
clsx({
'pt-4': index !== 0,
})
}
>
<GroupListItem group={group} withJoinAction />
</div>
), []);
const renderGroupGrid = useCallback((group: Group, index: number) => (
<div className='pb-4'>
<GroupGridItem group={group} />
</div>
), []);
return (
<Column
label={intl.formatMessage(messages.label)}
action={
<HStack alignItems='center'>
<button onClick={() => setLayout(Layout.LIST)}>
<Icon
src={require('@tabler/icons/layout-list.svg')}
className={
clsx('h-5 w-5 text-gray-600', {
'text-primary-600': layout === Layout.LIST,
})
}
/>
</button>
<button onClick={() => setLayout(Layout.GRID)}>
<Icon
src={require('@tabler/icons/layout-grid.svg')}
className={
clsx('h-5 w-5 text-gray-600', {
'text-primary-600': layout === Layout.GRID,
})
}
/>
</button>
</HStack>
}
>
{layout === Layout.LIST ? (
<Virtuoso
useWindowScroll
data={groups}
itemContent={(index, group) => renderGroupList(group, index)}
endReached={handleLoadMore}
/>
) : (
<VirtuosoGrid
useWindowScroll
data={groups}
itemContent={(index, group) => renderGroupGrid(group, index)}
components={{
Item: (props) => (
<div {...props} className='w-1/2 flex-none' />
),
List: GridList,
}}
endReached={handleLoadMore}
/>
)}
</Column>
);
};
export default Suggested;

Wyświetl plik

@ -118,6 +118,8 @@ import {
Events,
Groups,
GroupsDiscover,
GroupsPopular,
GroupsSuggested,
PendingGroupRequests,
GroupMembers,
GroupTimeline,
@ -289,6 +291,8 @@ const SwitchingColumnsArea: React.FC<ISwitchingColumnsArea> = ({ children }) =>
{features.groups && <WrappedRoute path='/groups' exact page={GroupsPage} component={Groups} content={children} />}
{features.groupsDiscovery && <WrappedRoute path='/groups/discover' exact page={GroupsPage} component={GroupsDiscover} content={children} />}
{features.groupsDiscovery && <WrappedRoute path='/groups/popular' exact page={GroupsPendingPage} component={GroupsPopular} content={children} />}
{features.groupsDiscovery && <WrappedRoute path='/groups/suggested' exact page={GroupsPendingPage} component={GroupsSuggested} content={children} />}
{features.groupsPending && <WrappedRoute path='/groups/pending-requests' exact page={GroupsPendingPage} component={PendingGroupRequests} content={children} />}
{features.groups && <WrappedRoute path='/groups/:id' exact page={GroupPage} component={GroupTimeline} content={children} />}
{features.groups && <WrappedRoute path='/groups/:id/members' exact page={GroupPage} component={GroupMembers} content={children} />}

Wyświetl plik

@ -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');
}

Wyświetl plik

@ -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}",