mirror of
https://github.com/glitch-soc/mastodon.git
synced 2026-03-29 03:00:33 +02:00
Add collection detail page (#37897)
This commit is contained in:
@@ -11,14 +11,14 @@ export interface ApiCollectionJSON {
|
||||
account_id: string;
|
||||
|
||||
id: string;
|
||||
uri: string;
|
||||
uri: string | null;
|
||||
local: boolean;
|
||||
item_count: number;
|
||||
|
||||
name: string;
|
||||
description: string;
|
||||
tag?: ApiTagJSON;
|
||||
language: string;
|
||||
tag: ApiTagJSON | null;
|
||||
language: string | null;
|
||||
sensitive: boolean;
|
||||
discoverable: boolean;
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||
|
||||
exports[`<Avatar /> > Autoplay > renders a animated avatar 1`] = `
|
||||
<div
|
||||
<span
|
||||
className="account__avatar account__avatar--loading"
|
||||
onMouseEnter={[Function]}
|
||||
onMouseLeave={[Function]}
|
||||
@@ -18,11 +18,11 @@ exports[`<Avatar /> > Autoplay > renders a animated avatar 1`] = `
|
||||
onLoad={[Function]}
|
||||
src="/animated/alice.gif"
|
||||
/>
|
||||
</div>
|
||||
</span>
|
||||
`;
|
||||
|
||||
exports[`<Avatar /> > Still > renders a still avatar 1`] = `
|
||||
<div
|
||||
<span
|
||||
className="account__avatar account__avatar--loading"
|
||||
onMouseEnter={[Function]}
|
||||
onMouseLeave={[Function]}
|
||||
@@ -39,5 +39,5 @@ exports[`<Avatar /> > Still > renders a still avatar 1`] = `
|
||||
onLoad={[Function]}
|
||||
src="/static/alice.jpg"
|
||||
/>
|
||||
</div>
|
||||
</span>
|
||||
`;
|
||||
|
||||
@@ -297,7 +297,7 @@ export const Account: React.FC<AccountProps> = ({
|
||||
>
|
||||
<div className='account__info-wrapper'>
|
||||
<Link
|
||||
className='account__display-name'
|
||||
className='account__display-name focusable'
|
||||
title={account?.acct}
|
||||
to={`/@${account?.acct}`}
|
||||
data-hover-card-account={id}
|
||||
|
||||
@@ -53,7 +53,7 @@ export const Avatar: React.FC<Props> = ({
|
||||
}, [setError]);
|
||||
|
||||
const avatar = (
|
||||
<div
|
||||
<span
|
||||
className={classNames(className, 'account__avatar', {
|
||||
'account__avatar--inline': inline,
|
||||
'account__avatar--loading': loading,
|
||||
@@ -67,14 +67,14 @@ export const Avatar: React.FC<Props> = ({
|
||||
)}
|
||||
|
||||
{counter && (
|
||||
<div
|
||||
<span
|
||||
className='account__avatar__counter'
|
||||
style={{ borderColor: counterBorderColor }}
|
||||
>
|
||||
{counter}
|
||||
</div>
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</span>
|
||||
);
|
||||
|
||||
if (withLink) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
.collectionItemWrapper {
|
||||
.wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
@@ -7,13 +7,13 @@
|
||||
border-bottom: 1px solid var(--color-border-primary);
|
||||
}
|
||||
|
||||
.collectionItemContent {
|
||||
.content {
|
||||
position: relative;
|
||||
flex-grow: 1;
|
||||
padding: 15px 5px;
|
||||
}
|
||||
|
||||
.collectionItemLink {
|
||||
.link {
|
||||
display: block;
|
||||
margin-bottom: 2px;
|
||||
font-size: 15px;
|
||||
@@ -33,16 +33,19 @@
|
||||
}
|
||||
}
|
||||
|
||||
.collectionItemInfo {
|
||||
.info {
|
||||
font-size: 13px;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.metaList {
|
||||
--gap: 0.75ch;
|
||||
|
||||
display: flex;
|
||||
gap: var(--gap);
|
||||
font-size: 13px;
|
||||
color: var(--color-text-secondary);
|
||||
|
||||
& > li:not(:first-child)::before {
|
||||
& > li:not(:last-child)::after {
|
||||
content: '·';
|
||||
margin-inline-end: var(--gap);
|
||||
margin-inline-start: var(--gap);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
import { useId } from 'react';
|
||||
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
import classNames from 'classnames';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import type { ApiCollectionJSON } from 'mastodon/api_types/collections';
|
||||
import { RelativeTimestamp } from 'mastodon/components/relative_timestamp';
|
||||
|
||||
import classes from './collection_list_item.module.scss';
|
||||
import { CollectionMenu } from './collection_menu';
|
||||
|
||||
export const CollectionMetaData: React.FC<{
|
||||
collection: ApiCollectionJSON;
|
||||
className?: string;
|
||||
}> = ({ collection, className }) => {
|
||||
return (
|
||||
<ul className={classNames(classes.metaList, className)}>
|
||||
<FormattedMessage
|
||||
id='collections.account_count'
|
||||
defaultMessage='{count, plural, one {# account} other {# accounts}}'
|
||||
values={{ count: collection.item_count }}
|
||||
tagName='li'
|
||||
/>
|
||||
<FormattedMessage
|
||||
id='collections.last_updated_at'
|
||||
defaultMessage='Last updated: {date}'
|
||||
values={{
|
||||
date: (
|
||||
<RelativeTimestamp
|
||||
timestamp={collection.updated_at}
|
||||
short={false}
|
||||
/>
|
||||
),
|
||||
}}
|
||||
tagName='li'
|
||||
/>
|
||||
</ul>
|
||||
);
|
||||
};
|
||||
|
||||
export const CollectionListItem: React.FC<{
|
||||
collection: ApiCollectionJSON;
|
||||
}> = ({ collection }) => {
|
||||
const { id, name } = collection;
|
||||
const linkId = useId();
|
||||
|
||||
return (
|
||||
<article
|
||||
className={classNames(classes.wrapper, 'focusable')}
|
||||
tabIndex={-1}
|
||||
aria-labelledby={linkId}
|
||||
>
|
||||
<div className={classes.content}>
|
||||
<h2 id={linkId}>
|
||||
<Link to={`/collections/${id}`} className={classes.link}>
|
||||
{name}
|
||||
</Link>
|
||||
</h2>
|
||||
<CollectionMetaData collection={collection} className={classes.info} />
|
||||
</div>
|
||||
|
||||
<CollectionMenu context='list' collection={collection} />
|
||||
</article>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,91 @@
|
||||
import { useCallback, useMemo } from 'react';
|
||||
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
|
||||
import MoreVertIcon from '@/material-icons/400-24px/more_vert.svg?react';
|
||||
import { openModal } from 'mastodon/actions/modal';
|
||||
import type { ApiCollectionJSON } from 'mastodon/api_types/collections';
|
||||
import { Dropdown } from 'mastodon/components/dropdown_menu';
|
||||
import { IconButton } from 'mastodon/components/icon_button';
|
||||
import { useAppDispatch } from 'mastodon/store';
|
||||
|
||||
import { messages as editorMessages } from '../editor';
|
||||
|
||||
const messages = defineMessages({
|
||||
view: {
|
||||
id: 'collections.view_collection',
|
||||
defaultMessage: 'View collection',
|
||||
},
|
||||
delete: {
|
||||
id: 'collections.delete_collection',
|
||||
defaultMessage: 'Delete collection',
|
||||
},
|
||||
more: { id: 'status.more', defaultMessage: 'More' },
|
||||
});
|
||||
|
||||
export const CollectionMenu: React.FC<{
|
||||
collection: ApiCollectionJSON;
|
||||
context: 'list' | 'collection';
|
||||
className?: string;
|
||||
}> = ({ collection, context, className }) => {
|
||||
const dispatch = useAppDispatch();
|
||||
const intl = useIntl();
|
||||
|
||||
const { id, name } = collection;
|
||||
|
||||
const handleDeleteClick = useCallback(() => {
|
||||
dispatch(
|
||||
openModal({
|
||||
modalType: 'CONFIRM_DELETE_COLLECTION',
|
||||
modalProps: {
|
||||
name,
|
||||
id,
|
||||
},
|
||||
}),
|
||||
);
|
||||
}, [dispatch, id, name]);
|
||||
|
||||
const menu = useMemo(() => {
|
||||
const commonItems = [
|
||||
{
|
||||
text: intl.formatMessage(editorMessages.manageAccounts),
|
||||
to: `/collections/${id}/edit`,
|
||||
},
|
||||
{
|
||||
text: intl.formatMessage(editorMessages.editDetails),
|
||||
to: `/collections/${id}/edit/details`,
|
||||
},
|
||||
{
|
||||
text: intl.formatMessage(editorMessages.editSettings),
|
||||
to: `/collections/${id}/edit/settings`,
|
||||
},
|
||||
null,
|
||||
{
|
||||
text: intl.formatMessage(messages.delete),
|
||||
action: handleDeleteClick,
|
||||
dangerous: true,
|
||||
},
|
||||
];
|
||||
|
||||
if (context === 'list') {
|
||||
return [
|
||||
{ text: intl.formatMessage(messages.view), to: `/collections/${id}` },
|
||||
null,
|
||||
...commonItems,
|
||||
];
|
||||
} else {
|
||||
return commonItems;
|
||||
}
|
||||
}, [intl, id, handleDeleteClick, context]);
|
||||
|
||||
return (
|
||||
<Dropdown scrollKey='collections' items={menu}>
|
||||
<IconButton
|
||||
icon='menu-icon'
|
||||
iconComponent={MoreVertIcon}
|
||||
title={intl.formatMessage(messages.more)}
|
||||
className={className}
|
||||
/>
|
||||
</Dropdown>
|
||||
);
|
||||
};
|
||||
178
app/javascript/mastodon/features/collections/detail/index.tsx
Normal file
178
app/javascript/mastodon/features/collections/detail/index.tsx
Normal file
@@ -0,0 +1,178 @@
|
||||
import { useCallback, useEffect } from 'react';
|
||||
|
||||
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
|
||||
|
||||
import { Helmet } from 'react-helmet';
|
||||
import { useParams } from 'react-router';
|
||||
|
||||
import ListAltIcon from '@/material-icons/400-24px/list_alt.svg?react';
|
||||
import ShareIcon from '@/material-icons/400-24px/share.svg?react';
|
||||
import { showAlert } from 'mastodon/actions/alerts';
|
||||
import type { ApiCollectionJSON } from 'mastodon/api_types/collections';
|
||||
import { Account } from 'mastodon/components/account';
|
||||
import { Avatar } from 'mastodon/components/avatar';
|
||||
import { Column } from 'mastodon/components/column';
|
||||
import { ColumnHeader } from 'mastodon/components/column_header';
|
||||
import { LinkedDisplayName } from 'mastodon/components/display_name';
|
||||
import { IconButton } from 'mastodon/components/icon_button';
|
||||
import ScrollableList from 'mastodon/components/scrollable_list';
|
||||
import { Tag } from 'mastodon/components/tags/tag';
|
||||
import { useAccount } from 'mastodon/hooks/useAccount';
|
||||
import { me } from 'mastodon/initial_state';
|
||||
import { fetchCollection } from 'mastodon/reducers/slices/collections';
|
||||
import { useAppDispatch, useAppSelector } from 'mastodon/store';
|
||||
|
||||
import { CollectionMetaData } from './collection_list_item';
|
||||
import { CollectionMenu } from './collection_menu';
|
||||
import classes from './styles.module.scss';
|
||||
|
||||
const messages = defineMessages({
|
||||
empty: {
|
||||
id: 'collections.accounts.empty_title',
|
||||
defaultMessage: 'This collection is empty',
|
||||
},
|
||||
loading: {
|
||||
id: 'collections.detail.loading',
|
||||
defaultMessage: 'Loading collection…',
|
||||
},
|
||||
share: {
|
||||
id: 'collections.detail.share',
|
||||
defaultMessage: 'Share this collection',
|
||||
},
|
||||
accounts: {
|
||||
id: 'collections.detail.accounts_heading',
|
||||
defaultMessage: 'Accounts',
|
||||
},
|
||||
});
|
||||
|
||||
const AuthorNote: React.FC<{ id: string }> = ({ id }) => {
|
||||
const account = useAccount(id);
|
||||
const author = (
|
||||
<span className={classes.displayNameWithAvatar}>
|
||||
<Avatar size={18} account={account} />
|
||||
<LinkedDisplayName displayProps={{ account, variant: 'simple' }} />
|
||||
</span>
|
||||
);
|
||||
|
||||
if (id === me) {
|
||||
return (
|
||||
<p className={classes.authorNote}>
|
||||
<FormattedMessage
|
||||
id='collections.detail.curated_by_you'
|
||||
defaultMessage='Curated by you'
|
||||
/>
|
||||
</p>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<p className={classes.authorNote}>
|
||||
<FormattedMessage
|
||||
id='collections.detail.curated_by_author'
|
||||
defaultMessage='Curated by {author}'
|
||||
values={{ author }}
|
||||
/>
|
||||
</p>
|
||||
);
|
||||
};
|
||||
|
||||
const CollectionHeader: React.FC<{ collection: ApiCollectionJSON }> = ({
|
||||
collection,
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
const { name, description, tag } = collection;
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const handleShare = useCallback(() => {
|
||||
dispatch(showAlert({ message: 'Collection sharing not yet implemented' }));
|
||||
}, [dispatch]);
|
||||
|
||||
return (
|
||||
<div className={classes.header}>
|
||||
<div className={classes.titleWithMenu}>
|
||||
<div className={classes.titleWrapper}>
|
||||
{tag && (
|
||||
// TODO: Make non-interactive tag component
|
||||
<Tag name={tag.name} className={classes.tag} />
|
||||
)}
|
||||
<h2 className={classes.name}>{name}</h2>
|
||||
</div>
|
||||
<div className={classes.headerButtonWrapper}>
|
||||
<IconButton
|
||||
iconComponent={ShareIcon}
|
||||
icon='share-icon'
|
||||
title={intl.formatMessage(messages.share)}
|
||||
className={classes.iconButton}
|
||||
onClick={handleShare}
|
||||
/>
|
||||
<CollectionMenu
|
||||
context='collection'
|
||||
collection={collection}
|
||||
className={classes.iconButton}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{description && <p className={classes.description}>{description}</p>}
|
||||
<AuthorNote id={collection.account_id} />
|
||||
<CollectionMetaData
|
||||
collection={collection}
|
||||
className={classes.metaData}
|
||||
/>
|
||||
<h2 className='sr-only'>{intl.formatMessage(messages.accounts)}</h2>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const CollectionDetailPage: React.FC<{
|
||||
multiColumn?: boolean;
|
||||
}> = ({ multiColumn }) => {
|
||||
const intl = useIntl();
|
||||
const dispatch = useAppDispatch();
|
||||
const { id } = useParams<{ id?: string }>();
|
||||
const collection = useAppSelector((state) =>
|
||||
id ? state.collections.collections[id] : undefined,
|
||||
);
|
||||
|
||||
const isLoading = !!id && !collection;
|
||||
|
||||
useEffect(() => {
|
||||
if (id) {
|
||||
void dispatch(fetchCollection({ collectionId: id }));
|
||||
}
|
||||
}, [dispatch, id]);
|
||||
|
||||
const pageTitle = collection?.name ?? intl.formatMessage(messages.loading);
|
||||
|
||||
return (
|
||||
<Column bindToDocument={!multiColumn} label={pageTitle}>
|
||||
<ColumnHeader
|
||||
showBackButton
|
||||
title={pageTitle}
|
||||
icon='collection-icon'
|
||||
iconComponent={ListAltIcon}
|
||||
multiColumn={multiColumn}
|
||||
/>
|
||||
|
||||
<ScrollableList
|
||||
scrollKey='collection-detail'
|
||||
emptyMessage={intl.formatMessage(messages.empty)}
|
||||
showLoading={isLoading}
|
||||
bindToDocument={!multiColumn}
|
||||
alwaysPrepend
|
||||
prepend={
|
||||
collection ? <CollectionHeader collection={collection} /> : null
|
||||
}
|
||||
>
|
||||
{collection?.items.map(({ account_id }) =>
|
||||
account_id ? (
|
||||
<Account key={account_id} minimal id={account_id} />
|
||||
) : null,
|
||||
)}
|
||||
</ScrollableList>
|
||||
|
||||
<Helmet>
|
||||
<title>{pageTitle}</title>
|
||||
<meta name='robots' content='noindex' />
|
||||
</Helmet>
|
||||
</Column>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,74 @@
|
||||
.header {
|
||||
padding: 16px;
|
||||
border-bottom: 1px solid var(--color-border-primary);
|
||||
}
|
||||
|
||||
.titleWithMenu {
|
||||
display: flex;
|
||||
align-items: start;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.titleWrapper {
|
||||
flex-grow: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.tag {
|
||||
margin-bottom: 4px;
|
||||
margin-inline-start: -8px;
|
||||
}
|
||||
|
||||
.name {
|
||||
font-size: 28px;
|
||||
line-height: 1.2;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.description {
|
||||
font-size: 15px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.headerButtonWrapper {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.iconButton {
|
||||
box-sizing: content-box;
|
||||
padding: 5px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--color-border-primary);
|
||||
}
|
||||
|
||||
.authorNote {
|
||||
margin-top: 8px;
|
||||
font-size: 13px;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.metaData {
|
||||
margin-top: 16px;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.displayNameWithAvatar {
|
||||
display: inline-flex;
|
||||
gap: 4px;
|
||||
align-items: baseline;
|
||||
|
||||
a {
|
||||
color: inherit;
|
||||
text-decoration: underline;
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
|
||||
> :global(.account__avatar) {
|
||||
align-self: center;
|
||||
}
|
||||
}
|
||||
@@ -68,7 +68,7 @@ export const CollectionDetails: React.FC<{
|
||||
};
|
||||
|
||||
void dispatch(updateCollection({ payload })).then(() => {
|
||||
history.push(`/collections`);
|
||||
history.push(`/collections/${id}`);
|
||||
});
|
||||
} else {
|
||||
const payload: Partial<ApiCreateCollectionPayload> = {
|
||||
|
||||
@@ -68,7 +68,7 @@ export const CollectionSettings: React.FC<{
|
||||
};
|
||||
|
||||
void dispatch(updateCollection({ payload })).then(() => {
|
||||
history.push(`/collections`);
|
||||
history.push(`/collections/${id}`);
|
||||
});
|
||||
} else {
|
||||
const payload: ApiCreateCollectionPayload = {
|
||||
|
||||
@@ -1,22 +1,16 @@
|
||||
import { useEffect, useMemo, useCallback, useId } from 'react';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
import { defineMessages, useIntl, FormattedMessage } from 'react-intl';
|
||||
|
||||
import classNames from 'classnames';
|
||||
import { Helmet } from 'react-helmet';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import AddIcon from '@/material-icons/400-24px/add.svg?react';
|
||||
import ListAltIcon from '@/material-icons/400-24px/list_alt.svg?react';
|
||||
import MoreHorizIcon from '@/material-icons/400-24px/more_horiz.svg?react';
|
||||
import SquigglyArrow from '@/svg-icons/squiggly_arrow.svg?react';
|
||||
import { openModal } from 'mastodon/actions/modal';
|
||||
import type { ApiCollectionJSON } from 'mastodon/api_types/collections';
|
||||
import { Column } from 'mastodon/components/column';
|
||||
import { ColumnHeader } from 'mastodon/components/column_header';
|
||||
import { Dropdown } from 'mastodon/components/dropdown_menu';
|
||||
import { Icon } from 'mastodon/components/icon';
|
||||
import { RelativeTimestamp } from 'mastodon/components/relative_timestamp';
|
||||
import ScrollableList from 'mastodon/components/scrollable_list';
|
||||
import {
|
||||
fetchAccountCollections,
|
||||
@@ -24,119 +18,13 @@ import {
|
||||
} from 'mastodon/reducers/slices/collections';
|
||||
import { useAppSelector, useAppDispatch } from 'mastodon/store';
|
||||
|
||||
import { CollectionListItem } from './detail/collection_list_item';
|
||||
import { messages as editorMessages } from './editor';
|
||||
import classes from './styles.module.scss';
|
||||
|
||||
const messages = defineMessages({
|
||||
heading: { id: 'column.collections', defaultMessage: 'My collections' },
|
||||
view: {
|
||||
id: 'collections.view_collection',
|
||||
defaultMessage: 'View collection',
|
||||
},
|
||||
delete: {
|
||||
id: 'collections.delete_collection',
|
||||
defaultMessage: 'Delete collection',
|
||||
},
|
||||
more: { id: 'status.more', defaultMessage: 'More' },
|
||||
});
|
||||
|
||||
const CollectionItem: React.FC<{
|
||||
collection: ApiCollectionJSON;
|
||||
}> = ({ collection }) => {
|
||||
const dispatch = useAppDispatch();
|
||||
const intl = useIntl();
|
||||
|
||||
const { id, name } = collection;
|
||||
|
||||
const handleDeleteClick = useCallback(() => {
|
||||
dispatch(
|
||||
openModal({
|
||||
modalType: 'CONFIRM_DELETE_COLLECTION',
|
||||
modalProps: {
|
||||
name,
|
||||
id,
|
||||
},
|
||||
}),
|
||||
);
|
||||
}, [dispatch, id, name]);
|
||||
|
||||
const menu = useMemo(
|
||||
() => [
|
||||
{ text: intl.formatMessage(messages.view), to: `/collections/${id}` },
|
||||
null,
|
||||
{
|
||||
text: intl.formatMessage(editorMessages.manageAccounts),
|
||||
to: `/collections/${id}/edit`,
|
||||
},
|
||||
{
|
||||
text: intl.formatMessage(editorMessages.editDetails),
|
||||
to: `/collections/${id}/edit/details`,
|
||||
},
|
||||
{
|
||||
text: intl.formatMessage(editorMessages.editSettings),
|
||||
to: `/collections/${id}/edit/settings`,
|
||||
},
|
||||
null,
|
||||
{
|
||||
text: intl.formatMessage(messages.delete),
|
||||
action: handleDeleteClick,
|
||||
dangerous: true,
|
||||
},
|
||||
],
|
||||
[intl, id, handleDeleteClick],
|
||||
);
|
||||
|
||||
const linkId = useId();
|
||||
|
||||
return (
|
||||
<article
|
||||
className={classNames(classes.collectionItemWrapper, 'focusable')}
|
||||
tabIndex={-1}
|
||||
aria-labelledby={linkId}
|
||||
>
|
||||
<div className={classes.collectionItemContent}>
|
||||
<h2 id={linkId}>
|
||||
<Link
|
||||
to={`/collections/${id}/edit/details`}
|
||||
className={classes.collectionItemLink}
|
||||
>
|
||||
{name}
|
||||
</Link>
|
||||
</h2>
|
||||
<ul className={classes.collectionItemInfo}>
|
||||
<FormattedMessage
|
||||
id='collections.account_count'
|
||||
defaultMessage='{count, plural, one {# account} other {# accounts}}'
|
||||
values={{ count: collection.item_count }}
|
||||
tagName='li'
|
||||
/>
|
||||
<FormattedMessage
|
||||
id='collections.last_updated_at'
|
||||
defaultMessage='Last updated: {date}'
|
||||
values={{
|
||||
date: (
|
||||
<RelativeTimestamp
|
||||
timestamp={collection.updated_at}
|
||||
short={false}
|
||||
/>
|
||||
),
|
||||
}}
|
||||
tagName='li'
|
||||
/>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<Dropdown
|
||||
scrollKey='collections'
|
||||
items={menu}
|
||||
icon='ellipsis-h'
|
||||
iconComponent={MoreHorizIcon}
|
||||
title={intl.formatMessage(messages.more)}
|
||||
/>
|
||||
</article>
|
||||
);
|
||||
};
|
||||
|
||||
export const Collections: React.FC<{
|
||||
multiColumn?: boolean;
|
||||
}> = ({ multiColumn }) => {
|
||||
@@ -202,7 +90,7 @@ export const Collections: React.FC<{
|
||||
bindToDocument={!multiColumn}
|
||||
>
|
||||
{collections.map((item) => (
|
||||
<CollectionItem key={item.id} collection={item} />
|
||||
<CollectionListItem key={item.id} collection={item} />
|
||||
))}
|
||||
</ScrollableList>
|
||||
|
||||
|
||||
@@ -65,6 +65,7 @@ import {
|
||||
ListEdit,
|
||||
ListMembers,
|
||||
Collections,
|
||||
CollectionDetail,
|
||||
CollectionsEditor,
|
||||
Blocks,
|
||||
DomainBlocks,
|
||||
@@ -269,12 +270,12 @@ class SwitchingColumnsArea extends PureComponent {
|
||||
<WrappedRoute path='/mutes' component={Mutes} content={children} />
|
||||
<WrappedRoute path='/lists' component={Lists} content={children} />
|
||||
{areCollectionsEnabled() &&
|
||||
<WrappedRoute path={['/collections/new', '/collections/:id/edit']} component={CollectionsEditor} content={children} />
|
||||
[
|
||||
<WrappedRoute path={['/collections/new', '/collections/:id/edit']} component={CollectionsEditor} content={children} />,
|
||||
<WrappedRoute path='/collections/:id' component={CollectionDetail} content={children} />,
|
||||
<WrappedRoute path='/collections' component={Collections} content={children} />
|
||||
]
|
||||
}
|
||||
{areCollectionsEnabled() &&
|
||||
<WrappedRoute path='/collections' component={Collections} content={children} />
|
||||
}
|
||||
|
||||
<Route component={BundleColumnError} />
|
||||
</WrappedSwitch>
|
||||
</ColumnsAreaContainer>
|
||||
|
||||
@@ -44,13 +44,19 @@ export function Lists () {
|
||||
return import('../../lists');
|
||||
}
|
||||
|
||||
export function Collections () {
|
||||
export function Collections() {
|
||||
return import('../../collections').then(
|
||||
module => ({default: module.Collections})
|
||||
);
|
||||
}
|
||||
|
||||
export function CollectionsEditor () {
|
||||
export function CollectionDetail() {
|
||||
return import('../../collections/detail/index').then(
|
||||
module => ({default: module.CollectionDetailPage})
|
||||
);
|
||||
}
|
||||
|
||||
export function CollectionsEditor() {
|
||||
return import('../../collections/editor').then(
|
||||
module => ({default: module.CollectionEditorPage})
|
||||
);
|
||||
|
||||
@@ -263,6 +263,11 @@
|
||||
"collections.create_collection": "Create collection",
|
||||
"collections.delete_collection": "Delete collection",
|
||||
"collections.description_length_hint": "100 characters limit",
|
||||
"collections.detail.accounts_heading": "Accounts",
|
||||
"collections.detail.curated_by_author": "Curated by {author}",
|
||||
"collections.detail.curated_by_you": "Curated by you",
|
||||
"collections.detail.loading": "Loading collection…",
|
||||
"collections.detail.share": "Share this collection",
|
||||
"collections.edit_details": "Edit basic details",
|
||||
"collections.edit_settings": "Edit settings",
|
||||
"collections.error_loading_collections": "There was an error when trying to load your collections.",
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="M480-160q-33 0-56.5-23.5T400-240q0-33 23.5-56.5T480-320q33 0 56.5 23.5T560-240q0 33-23.5 56.5T480-160Zm0-240q-33 0-56.5-23.5T400-480q0-33 23.5-56.5T480-560q33 0 56.5 23.5T560-480q0 33-23.5 56.5T480-400Zm0-240q-33 0-56.5-23.5T400-720q0-33 23.5-56.5T480-800q33 0 56.5 23.5T560-720q0 33-23.5 56.5T480-640Z"/></svg>
|
||||
|
After Width: | Height: | Size: 408 B |
1
app/javascript/material-icons/400-24px/more_vert.svg
Normal file
1
app/javascript/material-icons/400-24px/more_vert.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="M480-160q-33 0-56.5-23.5T400-240q0-33 23.5-56.5T480-320q33 0 56.5 23.5T560-240q0 33-23.5 56.5T480-160Zm0-240q-33 0-56.5-23.5T400-480q0-33 23.5-56.5T480-560q33 0 56.5 23.5T560-480q0 33-23.5 56.5T480-400Zm0-240q-33 0-56.5-23.5T400-720q0-33 23.5-56.5T480-800q33 0 56.5 23.5T560-720q0 33-23.5 56.5T480-640Z"/></svg>
|
||||
|
After Width: | Height: | Size: 408 B |
Reference in New Issue
Block a user