Add topic pages
This commit is contained in:
parent
8702c16998
commit
bb70c1ea3c
|
@ -0,0 +1,117 @@
|
||||||
|
import clsx from 'clsx';
|
||||||
|
import React, { useCallback, useState } from 'react';
|
||||||
|
import { Components, Virtuoso, VirtuosoGrid } from 'react-virtuoso';
|
||||||
|
|
||||||
|
import { Column, HStack, Icon } from 'soapbox/components/ui';
|
||||||
|
import { useGroupTag, useGroupsFromTag } from 'soapbox/hooks/api';
|
||||||
|
|
||||||
|
import GroupGridItem from './components/discover/group-grid-item';
|
||||||
|
import GroupListItem from './components/discover/group-list-item';
|
||||||
|
|
||||||
|
import type { Group } from 'soapbox/schemas';
|
||||||
|
|
||||||
|
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' />;
|
||||||
|
});
|
||||||
|
|
||||||
|
interface ITag {
|
||||||
|
params: { id: string }
|
||||||
|
}
|
||||||
|
|
||||||
|
const Tag: React.FC<ITag> = (props) => {
|
||||||
|
const tagId = props.params.id;
|
||||||
|
|
||||||
|
const [layout, setLayout] = useState<Layout>(Layout.LIST);
|
||||||
|
|
||||||
|
const { tag, isLoading } = useGroupTag(tagId);
|
||||||
|
const { groups, hasNextPage, fetchNextPage } = useGroupsFromTag(tagId);
|
||||||
|
|
||||||
|
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>
|
||||||
|
), []);
|
||||||
|
|
||||||
|
if (isLoading || !tag) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Column
|
||||||
|
label={`#${tag.name}`}
|
||||||
|
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 Tag;
|
|
@ -1,35 +1,24 @@
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import React, { useCallback, useState } from 'react';
|
import React from 'react';
|
||||||
import { Components, Virtuoso, VirtuosoGrid } from 'react-virtuoso';
|
import { FormattedMessage, defineMessages, useIntl } from 'react-intl';
|
||||||
|
import { Virtuoso } from 'react-virtuoso';
|
||||||
|
|
||||||
import { Column, HStack, Icon } from 'soapbox/components/ui';
|
import { Column, Text } from 'soapbox/components/ui';
|
||||||
import { useGroupsFromTag } from 'soapbox/hooks/api';
|
import { usePopularTags } from 'soapbox/hooks/api';
|
||||||
|
|
||||||
import GroupGridItem from './components/discover/group-grid-item';
|
import TagListItem from './components/discover/tag-list-item';
|
||||||
import GroupListItem from './components/discover/group-list-item';
|
|
||||||
|
|
||||||
import type { Group } from 'soapbox/schemas';
|
import type { GroupTag } from 'soapbox/schemas';
|
||||||
|
|
||||||
enum Layout {
|
const messages = defineMessages({
|
||||||
LIST = 'LIST',
|
title: { id: 'groups.tags.title', defaultMessage: 'Browse Topics' },
|
||||||
GRID = 'GRID'
|
|
||||||
}
|
|
||||||
|
|
||||||
const GridList: Components['List'] = React.forwardRef((props, ref) => {
|
|
||||||
const { context, ...rest } = props;
|
|
||||||
return <div ref={ref} {...rest} className='flex flex-wrap' />;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
interface ITags {
|
const Tags: React.FC = () => {
|
||||||
params: { id: string }
|
const intl = useIntl();
|
||||||
}
|
|
||||||
|
|
||||||
const Tags: React.FC<ITags> = (props) => {
|
const { tags, isFetched, isError, hasNextPage, fetchNextPage } = usePopularTags();
|
||||||
const tagId = props.params.id;
|
const isEmpty = (isFetched && tags.length === 0) || isError;
|
||||||
|
|
||||||
const [layout, setLayout] = useState<Layout>(Layout.LIST);
|
|
||||||
|
|
||||||
const { groups, hasNextPage, fetchNextPage } = useGroupsFromTag(tagId);
|
|
||||||
|
|
||||||
const handleLoadMore = () => {
|
const handleLoadMore = () => {
|
||||||
if (hasNextPage) {
|
if (hasNextPage) {
|
||||||
|
@ -37,7 +26,7 @@ const Tags: React.FC<ITags> = (props) => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderGroupList = useCallback((group: Group, index: number) => (
|
const renderItem = (index: number, tag: GroupTag) => (
|
||||||
<div
|
<div
|
||||||
className={
|
className={
|
||||||
clsx({
|
clsx({
|
||||||
|
@ -45,63 +34,24 @@ const Tags: React.FC<ITags> = (props) => {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<GroupListItem group={group} withJoinAction />
|
<TagListItem key={tag.id} tag={tag} />
|
||||||
</div>
|
</div>
|
||||||
), []);
|
);
|
||||||
|
|
||||||
const renderGroupGrid = useCallback((group: Group, index: number) => (
|
|
||||||
<div className='pb-4'>
|
|
||||||
<GroupGridItem group={group} />
|
|
||||||
</div>
|
|
||||||
), []);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Column
|
<Column label={intl.formatMessage(messages.title)}>
|
||||||
label={`#${tagId}`}
|
{isEmpty ? (
|
||||||
action={
|
<Text theme='muted'>
|
||||||
<HStack alignItems='center'>
|
<FormattedMessage
|
||||||
<button onClick={() => setLayout(Layout.LIST)}>
|
id='groups.discover.tags.empty'
|
||||||
<Icon
|
defaultMessage='Unable to fetch popular topics at this time. Please check back later.'
|
||||||
src={require('@tabler/icons/layout-list.svg')}
|
/>
|
||||||
className={
|
</Text>
|
||||||
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
|
<Virtuoso
|
||||||
useWindowScroll
|
useWindowScroll
|
||||||
data={groups}
|
data={tags}
|
||||||
itemContent={(index, group) => renderGroupList(group, index)}
|
itemContent={renderItem}
|
||||||
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}
|
endReached={handleLoadMore}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -122,6 +122,7 @@ import {
|
||||||
GroupsDiscover,
|
GroupsDiscover,
|
||||||
GroupsPopular,
|
GroupsPopular,
|
||||||
GroupsSuggested,
|
GroupsSuggested,
|
||||||
|
GroupsTag,
|
||||||
GroupsTags,
|
GroupsTags,
|
||||||
PendingGroupRequests,
|
PendingGroupRequests,
|
||||||
GroupMembers,
|
GroupMembers,
|
||||||
|
@ -297,7 +298,8 @@ const SwitchingColumnsArea: React.FC<ISwitchingColumnsArea> = ({ children }) =>
|
||||||
{features.groupsDiscovery && <WrappedRoute path='/groups/discover' exact page={GroupsPage} component={GroupsDiscover} 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/popular' exact page={GroupsPendingPage} component={GroupsPopular} content={children} />}
|
||||||
{features.groupsDiscovery && <WrappedRoute path='/groups/suggested' exact page={GroupsPendingPage} component={GroupsSuggested} content={children} />}
|
{features.groupsDiscovery && <WrappedRoute path='/groups/suggested' exact page={GroupsPendingPage} component={GroupsSuggested} content={children} />}
|
||||||
{features.groupsDiscovery && <WrappedRoute path='/groups/discover/tags/:id' exact page={GroupsPendingPage} component={GroupsTags} content={children} />}
|
{features.groupsDiscovery && <WrappedRoute path='/groups/tags' exact page={GroupsPendingPage} component={GroupsTags} content={children} />}
|
||||||
|
{features.groupsDiscovery && <WrappedRoute path='/groups/discover/tags/:id' exact page={GroupsPendingPage} component={GroupsTag} content={children} />}
|
||||||
{features.groupsPending && <WrappedRoute path='/groups/pending-requests' exact page={GroupsPendingPage} component={PendingGroupRequests} 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' exact page={GroupPage} component={GroupTimeline} content={children} />}
|
||||||
{features.groups && <WrappedRoute path='/groups/:id/members' exact page={GroupPage} component={GroupMembers} content={children} />}
|
{features.groups && <WrappedRoute path='/groups/:id/members' exact page={GroupPage} component={GroupMembers} content={children} />}
|
||||||
|
|
|
@ -562,6 +562,10 @@ export function GroupsSuggested() {
|
||||||
return import(/* webpackChunkName: "features/groups" */'../../groups/suggested');
|
return import(/* webpackChunkName: "features/groups" */'../../groups/suggested');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function GroupsTag() {
|
||||||
|
return import(/* webpackChunkName: "features/groups" */'../../groups/tag');
|
||||||
|
}
|
||||||
|
|
||||||
export function GroupsTags() {
|
export function GroupsTags() {
|
||||||
return import(/* webpackChunkName: "features/groups" */'../../groups/tags');
|
return import(/* webpackChunkName: "features/groups" */'../../groups/tags');
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,21 @@
|
||||||
|
import { Entities } from 'soapbox/entity-store/entities';
|
||||||
|
import { useEntity } from 'soapbox/entity-store/hooks';
|
||||||
|
import { useApi } from 'soapbox/hooks';
|
||||||
|
import { type GroupTag, groupTagSchema } from 'soapbox/schemas';
|
||||||
|
|
||||||
|
function useGroupTag(tagId: string) {
|
||||||
|
const api = useApi();
|
||||||
|
|
||||||
|
const { entity: tag, ...result } = useEntity<GroupTag>(
|
||||||
|
[Entities.GROUP_TAGS, tagId],
|
||||||
|
() => api.get(`/api/v1/tags/${tagId }`),
|
||||||
|
{ schema: groupTagSchema },
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...result,
|
||||||
|
tag,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export { useGroupTag };
|
|
@ -5,6 +5,8 @@ import { groupSchema } from 'soapbox/schemas';
|
||||||
import { useApi } from '../../useApi';
|
import { useApi } from '../../useApi';
|
||||||
import { useFeatures } from '../../useFeatures';
|
import { useFeatures } from '../../useFeatures';
|
||||||
|
|
||||||
|
import { useGroupRelationships } from './useGroups';
|
||||||
|
|
||||||
import type { Group } from 'soapbox/schemas';
|
import type { Group } from 'soapbox/schemas';
|
||||||
|
|
||||||
function useGroupsFromTag(tagId: string) {
|
function useGroupsFromTag(tagId: string) {
|
||||||
|
@ -13,16 +15,22 @@ function useGroupsFromTag(tagId: string) {
|
||||||
|
|
||||||
const { entities, ...result } = useEntities<Group>(
|
const { entities, ...result } = useEntities<Group>(
|
||||||
[Entities.GROUPS, 'tags', tagId],
|
[Entities.GROUPS, 'tags', tagId],
|
||||||
() => api.get(`/api/mock/tags/${tagId}/groups`),
|
() => api.get(`/api/v1/tags/${tagId}/groups`),
|
||||||
{
|
{
|
||||||
schema: groupSchema,
|
schema: groupSchema,
|
||||||
enabled: features.groupsDiscovery,
|
enabled: features.groupsDiscovery,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
const { relationships } = useGroupRelationships(entities.map(entity => entity.id));
|
||||||
|
|
||||||
|
const groups = entities.map((group) => ({
|
||||||
|
...group,
|
||||||
|
relationship: relationships[group.id] || null,
|
||||||
|
}));
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...result,
|
...result,
|
||||||
groups: entities,
|
groups,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -11,7 +11,7 @@ function usePopularTags() {
|
||||||
|
|
||||||
const { entities, ...result } = useEntities<GroupTag>(
|
const { entities, ...result } = useEntities<GroupTag>(
|
||||||
[Entities.GROUP_TAGS],
|
[Entities.GROUP_TAGS],
|
||||||
() => api.get('/api/mock/groups/tags'),
|
() => api.get('/api/v1/groups/tags'),
|
||||||
{
|
{
|
||||||
schema: groupTagSchema,
|
schema: groupTagSchema,
|
||||||
enabled: features.groupsDiscovery,
|
enabled: features.groupsDiscovery,
|
||||||
|
|
|
@ -15,6 +15,7 @@ export { useGroupMedia } from './groups/useGroupMedia';
|
||||||
export { useGroup, useGroups } from './groups/useGroups';
|
export { useGroup, useGroups } from './groups/useGroups';
|
||||||
export { useGroupMembershipRequests } from './groups/useGroupMembershipRequests';
|
export { useGroupMembershipRequests } from './groups/useGroupMembershipRequests';
|
||||||
export { useGroupSearch } from './groups/useGroupSearch';
|
export { useGroupSearch } from './groups/useGroupSearch';
|
||||||
|
export { useGroupTag } from './groups/useGroupTag';
|
||||||
export { useGroupValidation } from './groups/useGroupValidation';
|
export { useGroupValidation } from './groups/useGroupValidation';
|
||||||
export { useGroupsFromTag } from './groups/useGroupsFromTag';
|
export { useGroupsFromTag } from './groups/useGroupsFromTag';
|
||||||
export { useJoinGroup } from './groups/useJoinGroup';
|
export { useJoinGroup } from './groups/useJoinGroup';
|
||||||
|
|
|
@ -830,6 +830,10 @@
|
||||||
"groups.discover.suggested.empty": "Unable to fetch suggested groups at this time. Please check back later.",
|
"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.show_more": "Show More",
|
||||||
"groups.discover.suggested.title": "Suggested For You",
|
"groups.discover.suggested.title": "Suggested For You",
|
||||||
|
"groups.discover.tags.empty": "Unable to fetch popular topics at this time. Please check back later.",
|
||||||
|
"groups.discover.tags.show_more": "Show More",
|
||||||
|
"groups.discover.tags.title": "Browse Topics",
|
||||||
|
"groups.discovery.tags.no_of_groups": "Number of groups",
|
||||||
"groups.empty.subtitle": "Start discovering groups to join or create your own.",
|
"groups.empty.subtitle": "Start discovering groups to join or create your own.",
|
||||||
"groups.empty.title": "No Groups yet",
|
"groups.empty.title": "No Groups yet",
|
||||||
"groups.pending.count": "{number, plural, one {# pending request} other {# pending requests}}",
|
"groups.pending.count": "{number, plural, one {# pending request} other {# pending requests}}",
|
||||||
|
@ -838,6 +842,7 @@
|
||||||
"groups.pending.label": "Pending Requests",
|
"groups.pending.label": "Pending Requests",
|
||||||
"groups.popular.label": "Suggested Groups",
|
"groups.popular.label": "Suggested Groups",
|
||||||
"groups.search.placeholder": "Search My Groups",
|
"groups.search.placeholder": "Search My Groups",
|
||||||
|
"groups.tags.title": "Browse Topics",
|
||||||
"hashtag.column_header.tag_mode.all": "and {additional}",
|
"hashtag.column_header.tag_mode.all": "and {additional}",
|
||||||
"hashtag.column_header.tag_mode.any": "or {additional}",
|
"hashtag.column_header.tag_mode.any": "or {additional}",
|
||||||
"hashtag.column_header.tag_mode.none": "without {additional}",
|
"hashtag.column_header.tag_mode.none": "without {additional}",
|
||||||
|
|
Loading…
Reference in New Issue