Update design of collection accounts editor (#38712)
This commit is contained in:
parent
0ef00be494
commit
5a38246ee8
@ -35,3 +35,12 @@
|
||||
text-wrap: pretty;
|
||||
}
|
||||
}
|
||||
|
||||
[data-color-scheme='dark'] .defaultImage {
|
||||
--color-skin-1: #3a3a50;
|
||||
--color-skin-2: #67678e;
|
||||
--color-skin-3: #44445f;
|
||||
--color-outline: #21212c;
|
||||
--color-shadow: #181820;
|
||||
--color-highlight: #b2b1c8;
|
||||
}
|
||||
|
||||
@ -29,12 +29,6 @@ export const Default: Story = {
|
||||
},
|
||||
};
|
||||
|
||||
export const WithoutMessage: Story = {
|
||||
args: {
|
||||
message: undefined,
|
||||
},
|
||||
};
|
||||
|
||||
export const WithAction: Story = {
|
||||
args: {
|
||||
...Default.args,
|
||||
@ -42,3 +36,17 @@ export const WithAction: Story = {
|
||||
children: <Button onClick={() => action('Refresh')}>Refresh</Button>,
|
||||
},
|
||||
};
|
||||
|
||||
export const WithoutImage: Story = {
|
||||
args: {
|
||||
...Default.args,
|
||||
image: null,
|
||||
},
|
||||
};
|
||||
|
||||
export const WithoutMessage: Story = {
|
||||
args: {
|
||||
...Default.args,
|
||||
message: undefined,
|
||||
},
|
||||
};
|
||||
|
||||
@ -1,31 +1,39 @@
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
import ElephantImage from '@/images/elephant_ui.svg?react';
|
||||
|
||||
import classes from './empty_state.module.scss';
|
||||
|
||||
const images = {
|
||||
default: <ElephantImage className={classes.defaultImage} />,
|
||||
};
|
||||
|
||||
/**
|
||||
* Simple empty state component with a neutral default title and customisable message.
|
||||
*
|
||||
* Action buttons can be passed as `children`
|
||||
* Action buttons can be passed as `children`.
|
||||
*/
|
||||
|
||||
export const EmptyState: React.FC<{
|
||||
image?: React.ReactNode;
|
||||
image?: keyof typeof images | React.ReactElement | null;
|
||||
title?: React.ReactNode;
|
||||
message?: React.ReactNode;
|
||||
children?: React.ReactNode;
|
||||
}> = ({
|
||||
image,
|
||||
image = 'default',
|
||||
title = (
|
||||
<FormattedMessage id='empty_state.no_results' defaultMessage='No results' />
|
||||
),
|
||||
message,
|
||||
children,
|
||||
}) => {
|
||||
const imageToRender = typeof image === 'string' ? images[image] : image;
|
||||
|
||||
return (
|
||||
<div className={classes.wrapper}>
|
||||
{(title || message || image) && (
|
||||
{(title || message || imageToRender) && (
|
||||
<div className={classes.content}>
|
||||
{image}
|
||||
{imageToRender}
|
||||
{!!title && <h3>{title}</h3>}
|
||||
{!!message && <p>{message}</p>}
|
||||
</div>
|
||||
|
||||
@ -1,8 +0,0 @@
|
||||
[data-color-scheme='dark'] .image {
|
||||
--color-skin-1: #3a3a50;
|
||||
--color-skin-2: #67678e;
|
||||
--color-skin-3: #44445f;
|
||||
--color-outline: #21212c;
|
||||
--color-shadow: #181820;
|
||||
--color-highlight: #b2b1c8;
|
||||
}
|
||||
@ -5,7 +5,6 @@ import { FormattedMessage } from 'react-intl';
|
||||
import { useParams } from 'react-router';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import ElephantImage from '@/images/elephant_ui.svg?react';
|
||||
import { openModal } from '@/mastodon/actions/modal';
|
||||
import { Button } from '@/mastodon/components/button';
|
||||
import { EmptyState } from '@/mastodon/components/empty_state';
|
||||
@ -14,8 +13,6 @@ import { areCollectionsEnabled } from '@/mastodon/features/collections/utils';
|
||||
import { useCurrentAccountId } from '@/mastodon/hooks/useAccountId';
|
||||
import { useAppDispatch } from '@/mastodon/store';
|
||||
|
||||
import classes from './empty_message.module.scss';
|
||||
|
||||
interface EmptyMessageProps {
|
||||
suspended: boolean;
|
||||
hidden: boolean;
|
||||
@ -54,14 +51,11 @@ export const EmptyMessage: React.FC<EmptyMessageProps> = ({
|
||||
|
||||
const hasCollections = areCollectionsEnabled();
|
||||
|
||||
const image = <ElephantImage className={classes.image} />;
|
||||
|
||||
if (me === accountId) {
|
||||
if (hasCollections) {
|
||||
// Return only here to insert the "Create a collection" button as the action for the empty state.
|
||||
return (
|
||||
<EmptyState
|
||||
image={image}
|
||||
title={
|
||||
<FormattedMessage
|
||||
id='empty_column.account_featured_self.showcase_accounts'
|
||||
@ -140,5 +134,5 @@ export const EmptyMessage: React.FC<EmptyMessageProps> = ({
|
||||
}
|
||||
}
|
||||
|
||||
return <EmptyState title={title} message={message} image={image} />;
|
||||
return <EmptyState title={title} message={message} />;
|
||||
};
|
||||
|
||||
@ -4,22 +4,16 @@ import { FormattedMessage, useIntl } from 'react-intl';
|
||||
|
||||
import { useHistory } from 'react-router-dom';
|
||||
|
||||
import CancelIcon from '@/material-icons/400-24px/cancel.svg?react';
|
||||
import CheckIcon from '@/material-icons/400-24px/check.svg?react';
|
||||
import WarningIcon from '@/material-icons/400-24px/warning.svg?react';
|
||||
import { showAlertForError } from 'mastodon/actions/alerts';
|
||||
import { openModal } from 'mastodon/actions/modal';
|
||||
import { apiFollowAccount } from 'mastodon/api/accounts';
|
||||
import type { ApiCollectionJSON } from 'mastodon/api_types/collections';
|
||||
import { Account } from 'mastodon/components/account';
|
||||
import { Avatar } from 'mastodon/components/avatar';
|
||||
import { Badge } from 'mastodon/components/badge';
|
||||
import { Button } from 'mastodon/components/button';
|
||||
import { DisplayName } from 'mastodon/components/display_name';
|
||||
import { EmptyState } from 'mastodon/components/empty_state';
|
||||
import { FormStack, Combobox } from 'mastodon/components/form_fields';
|
||||
import { Icon } from 'mastodon/components/icon';
|
||||
import { IconButton } from 'mastodon/components/icon_button';
|
||||
import { FormStack, ComboboxField } from 'mastodon/components/form_fields';
|
||||
import {
|
||||
Article,
|
||||
ItemList,
|
||||
@ -37,75 +31,35 @@ import {
|
||||
import { store, useAppDispatch, useAppSelector } from 'mastodon/store';
|
||||
|
||||
import classes from './styles.module.scss';
|
||||
import { WizardStepHeader } from './wizard_step_header';
|
||||
import { WizardStepTitle } from './wizard_step_title';
|
||||
|
||||
const MAX_ACCOUNT_COUNT = 25;
|
||||
|
||||
function isOlderThanAWeek(date?: string): boolean {
|
||||
if (!date) return false;
|
||||
|
||||
const targetDate = new Date(date);
|
||||
const sevenDaysAgo = new Date();
|
||||
sevenDaysAgo.setDate(sevenDaysAgo.getDate() - 7);
|
||||
return targetDate < sevenDaysAgo;
|
||||
}
|
||||
const MAX_ACCOUNT_COUNT = 3;
|
||||
|
||||
const AddedAccountItem: React.FC<{
|
||||
accountId: string;
|
||||
onRemove: (id: string) => void;
|
||||
}> = ({ accountId, onRemove }) => {
|
||||
const intl = useIntl();
|
||||
const account = useAccount(accountId);
|
||||
|
||||
const handleRemoveAccount = useCallback(() => {
|
||||
onRemove(accountId);
|
||||
}, [accountId, onRemove]);
|
||||
|
||||
const lastStatusAt = account?.last_status_at;
|
||||
|
||||
const lastPostHint = useMemo(
|
||||
() =>
|
||||
(!lastStatusAt || isOlderThanAWeek(lastStatusAt)) && (
|
||||
<Badge
|
||||
label={
|
||||
<FormattedMessage
|
||||
id='collections.old_last_post_note'
|
||||
defaultMessage='Last posted over a week ago'
|
||||
/>
|
||||
}
|
||||
icon={<WarningIcon />}
|
||||
className={classes.accountBadge}
|
||||
/>
|
||||
),
|
||||
[lastStatusAt],
|
||||
);
|
||||
|
||||
return (
|
||||
<Account
|
||||
minimal
|
||||
key={accountId}
|
||||
id={accountId}
|
||||
extraAccountInfo={lastPostHint}
|
||||
>
|
||||
<IconButton
|
||||
title={intl.formatMessage({
|
||||
id: 'collections.remove_account',
|
||||
defaultMessage: 'Remove this account',
|
||||
})}
|
||||
icon='remove'
|
||||
iconComponent={CancelIcon}
|
||||
onClick={handleRemoveAccount}
|
||||
/>
|
||||
<Account minimal key={accountId} id={accountId}>
|
||||
<Button compact secondary onClick={handleRemoveAccount}>
|
||||
<FormattedMessage
|
||||
id='collections.remove_account'
|
||||
defaultMessage='Remove'
|
||||
/>
|
||||
</Button>
|
||||
</Account>
|
||||
);
|
||||
};
|
||||
|
||||
interface SuggestionItem {
|
||||
id: string;
|
||||
isSelected: boolean;
|
||||
}
|
||||
|
||||
const SuggestedAccountItem: React.FC<SuggestionItem> = ({ id, isSelected }) => {
|
||||
const SuggestedAccountItem: React.FC<SuggestionItem> = ({ id }) => {
|
||||
const account = useAccount(id);
|
||||
|
||||
if (!account) return null;
|
||||
@ -114,23 +68,15 @@ const SuggestedAccountItem: React.FC<SuggestionItem> = ({ id, isSelected }) => {
|
||||
<>
|
||||
<Avatar account={account} />
|
||||
<DisplayName account={account} />
|
||||
{isSelected && (
|
||||
<Icon
|
||||
id='checked'
|
||||
icon={CheckIcon}
|
||||
className={classes.selectedSuggestionIcon}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const renderAccountItem = (item: SuggestionItem) => (
|
||||
<SuggestedAccountItem id={item.id} isSelected={item.isSelected} />
|
||||
<SuggestedAccountItem id={item.id} />
|
||||
);
|
||||
|
||||
const getItemId = (item: SuggestionItem) => item.id;
|
||||
const getIsItemSelected = (item: SuggestionItem) => item.isSelected;
|
||||
|
||||
export const CollectionAccounts: React.FC<{
|
||||
collection?: ApiCollectionJSON | null;
|
||||
@ -139,9 +85,8 @@ export const CollectionAccounts: React.FC<{
|
||||
const dispatch = useAppDispatch();
|
||||
const history = useHistory();
|
||||
|
||||
const { id, items } = collection ?? {};
|
||||
const { id, items: collectionItems } = collection ?? {};
|
||||
const isEditMode = !!id;
|
||||
const collectionItems = items;
|
||||
|
||||
const addedAccountIds = useAppSelector(
|
||||
(state) => state.collections.editor.accountIds,
|
||||
@ -157,22 +102,25 @@ export const CollectionAccounts: React.FC<{
|
||||
|
||||
const [searchValue, setSearchValue] = useState('');
|
||||
|
||||
const hasAccounts = accountIds.length > 0;
|
||||
const hasMaxAccounts = accountIds.length === MAX_ACCOUNT_COUNT;
|
||||
|
||||
const {
|
||||
accountIds: suggestedAccountIds,
|
||||
isLoading: isLoadingSuggestions,
|
||||
searchAccounts,
|
||||
resetAccounts,
|
||||
} = useSearchAccounts({
|
||||
withRelationships: true,
|
||||
filterResults: (account) =>
|
||||
!accountIds.includes(account.id) &&
|
||||
// Only suggest accounts who allow being featured/recommended
|
||||
account.feature_approval.current_user === 'automatic',
|
||||
});
|
||||
|
||||
const suggestedItems = suggestedAccountIds.map((id) => ({
|
||||
id,
|
||||
isSelected: accountIds.includes(id),
|
||||
isDisabled: accountIds.includes(id),
|
||||
}));
|
||||
|
||||
const handleSearchValueChange = useCallback(
|
||||
@ -242,12 +190,12 @@ export const CollectionAccounts: React.FC<{
|
||||
);
|
||||
|
||||
const addAccountItem = useCallback(
|
||||
(accountId: string) => {
|
||||
confirmFollowStatus(accountId, () => {
|
||||
(item: SuggestionItem) => {
|
||||
confirmFollowStatus(item.id, () => {
|
||||
dispatch(
|
||||
updateCollectionEditorField({
|
||||
field: 'accountIds',
|
||||
value: [...accountIds, accountId],
|
||||
value: [...accountIds, item.id],
|
||||
}),
|
||||
);
|
||||
});
|
||||
@ -255,17 +203,6 @@ export const CollectionAccounts: React.FC<{
|
||||
[accountIds, confirmFollowStatus, dispatch],
|
||||
);
|
||||
|
||||
const toggleAccountItem = useCallback(
|
||||
(item: SuggestionItem) => {
|
||||
if (accountIds.includes(item.id)) {
|
||||
removeAccountItem(item.id);
|
||||
} else {
|
||||
addAccountItem(item.id);
|
||||
}
|
||||
},
|
||||
[accountIds, addAccountItem, removeAccountItem],
|
||||
);
|
||||
|
||||
const instantRemoveAccountItem = useCallback(
|
||||
(accountId: string) => {
|
||||
const itemId = collectionItems?.find(
|
||||
@ -289,23 +226,16 @@ export const CollectionAccounts: React.FC<{
|
||||
);
|
||||
|
||||
const instantAddAccountItem = useCallback(
|
||||
(collectionId: string, accountId: string) => {
|
||||
confirmFollowStatus(accountId, () => {
|
||||
void dispatch(addCollectionItem({ collectionId, accountId }));
|
||||
(item: SuggestionItem) => {
|
||||
confirmFollowStatus(item.id, () => {
|
||||
if (id) {
|
||||
void dispatch(
|
||||
addCollectionItem({ collectionId: id, accountId: item.id }),
|
||||
);
|
||||
}
|
||||
});
|
||||
},
|
||||
[confirmFollowStatus, dispatch],
|
||||
);
|
||||
|
||||
const instantToggleAccountItem = useCallback(
|
||||
(item: SuggestionItem) => {
|
||||
if (accountIds.includes(item.id)) {
|
||||
instantRemoveAccountItem(item.id);
|
||||
} else if (id) {
|
||||
instantAddAccountItem(id, item.id);
|
||||
}
|
||||
},
|
||||
[accountIds, id, instantAddAccountItem, instantRemoveAccountItem],
|
||||
[confirmFollowStatus, dispatch, id],
|
||||
);
|
||||
|
||||
const handleRemoveAccountItem = useCallback(
|
||||
@ -319,6 +249,20 @@ export const CollectionAccounts: React.FC<{
|
||||
[isEditMode, instantRemoveAccountItem, removeAccountItem],
|
||||
);
|
||||
|
||||
const handleSelectItem = useCallback(
|
||||
(item: SuggestionItem) => {
|
||||
if (isEditMode) {
|
||||
instantAddAccountItem(item);
|
||||
} else {
|
||||
addAccountItem(item);
|
||||
}
|
||||
|
||||
setSearchValue('');
|
||||
resetAccounts();
|
||||
},
|
||||
[addAccountItem, instantAddAccountItem, isEditMode, resetAccounts],
|
||||
);
|
||||
|
||||
const handleSubmit = useCallback(
|
||||
(e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
@ -333,117 +277,114 @@ export const CollectionAccounts: React.FC<{
|
||||
);
|
||||
|
||||
const inputId = useId();
|
||||
const inputLabel = intl.formatMessage({
|
||||
id: 'collections.search_accounts_label',
|
||||
defaultMessage: 'Search for accounts to add…',
|
||||
});
|
||||
const AccountsHeadingElement = id ? 'h2' : 'h3';
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit} className={classes.form}>
|
||||
<FormStack className={classes.formFieldStack}>
|
||||
{!id && (
|
||||
<WizardStepHeader
|
||||
step={1}
|
||||
title={
|
||||
<FormattedMessage
|
||||
id='collections.create.accounts_title'
|
||||
defaultMessage='Who will you feature in this collection?'
|
||||
/>
|
||||
}
|
||||
description={
|
||||
<FormattedMessage
|
||||
id='collections.create.accounts_subtitle'
|
||||
defaultMessage='Only accounts you follow who have opted into discovery can be added.'
|
||||
/>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
<label htmlFor={inputId} className='sr-only'>
|
||||
{inputLabel}
|
||||
</label>
|
||||
<Combobox
|
||||
id={inputId}
|
||||
placeholder={inputLabel}
|
||||
value={hasMaxAccounts ? '' : searchValue}
|
||||
onChange={handleSearchValueChange}
|
||||
onKeyDown={handleSearchKeyDown}
|
||||
disabled={hasMaxAccounts}
|
||||
isLoading={isLoadingSuggestions}
|
||||
items={suggestedItems}
|
||||
getItemId={getItemId}
|
||||
getIsItemSelected={getIsItemSelected}
|
||||
renderItem={renderAccountItem}
|
||||
onSelectItem={
|
||||
isEditMode ? instantToggleAccountItem : toggleAccountItem
|
||||
}
|
||||
closeOnSelect={false}
|
||||
/>
|
||||
{hasMaxAccounts && (
|
||||
<FormattedMessage
|
||||
id='collections.search_accounts_max_reached'
|
||||
defaultMessage='You have added the maximum number of accounts'
|
||||
/>
|
||||
)}
|
||||
|
||||
<Scrollable className={classes.scrollableWrapper}>
|
||||
<ItemList
|
||||
className={classes.scrollableInner}
|
||||
emptyMessage={
|
||||
<EmptyState
|
||||
title={
|
||||
<FormattedMessage
|
||||
id='collections.accounts.empty_title'
|
||||
defaultMessage='This collection is empty'
|
||||
/>
|
||||
}
|
||||
message={
|
||||
<FormattedMessage
|
||||
id='collections.accounts.empty_description'
|
||||
defaultMessage='Add up to {count} accounts you follow'
|
||||
values={{
|
||||
count: MAX_ACCOUNT_COUNT,
|
||||
}}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
}
|
||||
>
|
||||
{accountIds.map((accountId, index) => (
|
||||
<Article
|
||||
key={accountId}
|
||||
aria-posinset={index}
|
||||
aria-setsize={accountIds.length}
|
||||
>
|
||||
<AddedAccountItem
|
||||
accountId={accountId}
|
||||
onRemove={handleRemoveAccountItem}
|
||||
/>
|
||||
</Article>
|
||||
))}
|
||||
</ItemList>
|
||||
</Scrollable>
|
||||
</FormStack>
|
||||
{!isEditMode && (
|
||||
<div className={classes.stickyFooter}>
|
||||
<div className={classes.actionWrapper}>
|
||||
<FormattedMessage
|
||||
id='collections.hints.accounts_counter'
|
||||
defaultMessage='{count} / {max} accounts'
|
||||
values={{ count: accountIds.length, max: MAX_ACCOUNT_COUNT }}
|
||||
>
|
||||
{(text) => <div className={classes.itemCountReadout}>{text}</div>}
|
||||
</FormattedMessage>
|
||||
<Button type='submit'>
|
||||
{id ? (
|
||||
<FormattedMessage id='lists.save' defaultMessage='Save' />
|
||||
) : (
|
||||
<header className={classes.header}>
|
||||
{!id && (
|
||||
<WizardStepTitle
|
||||
step={1}
|
||||
title={
|
||||
<FormattedMessage
|
||||
id='collections.continue'
|
||||
defaultMessage='Continue'
|
||||
id='collections.create.accounts_title'
|
||||
defaultMessage='Who will you feature in this collection?'
|
||||
/>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
<ComboboxField
|
||||
id={inputId}
|
||||
label={intl.formatMessage({
|
||||
id: 'collections.search_accounts_label',
|
||||
defaultMessage: 'Search for an account to add',
|
||||
})}
|
||||
value={hasMaxAccounts ? '' : searchValue}
|
||||
onChange={handleSearchValueChange}
|
||||
onKeyDown={handleSearchKeyDown}
|
||||
disabled={hasMaxAccounts}
|
||||
isLoading={isLoadingSuggestions}
|
||||
items={suggestedItems}
|
||||
getItemId={getItemId}
|
||||
renderItem={renderAccountItem}
|
||||
onSelectItem={handleSelectItem}
|
||||
status={
|
||||
hasMaxAccounts
|
||||
? {
|
||||
variant: 'warning',
|
||||
message: intl.formatMessage({
|
||||
id: 'collections.search_accounts_max_reached',
|
||||
defaultMessage:
|
||||
'You have added the maximum number of accounts',
|
||||
}),
|
||||
}
|
||||
: null
|
||||
}
|
||||
/>
|
||||
</header>
|
||||
|
||||
<div>
|
||||
{hasAccounts && (
|
||||
<AccountsHeadingElement className={classes.listHeading}>
|
||||
<FormattedMessage
|
||||
id='collections.hints.accounts_counter'
|
||||
defaultMessage='{count}/{max} accounts'
|
||||
values={{ count: accountIds.length, max: MAX_ACCOUNT_COUNT }}
|
||||
/>
|
||||
</AccountsHeadingElement>
|
||||
)}
|
||||
|
||||
<Scrollable className={classes.scrollableWrapper}>
|
||||
<ItemList
|
||||
emptyMessage={
|
||||
<EmptyState
|
||||
title={
|
||||
<FormattedMessage
|
||||
id='collections.accounts.empty_editor_title'
|
||||
defaultMessage='No one is in this collection yet'
|
||||
/>
|
||||
}
|
||||
message={
|
||||
<FormattedMessage
|
||||
id='collections.accounts.empty_description'
|
||||
defaultMessage='Add up to {count} accounts'
|
||||
values={{
|
||||
count: MAX_ACCOUNT_COUNT,
|
||||
}}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
}
|
||||
>
|
||||
{accountIds.map((accountId, index) => (
|
||||
<Article
|
||||
key={accountId}
|
||||
aria-posinset={index}
|
||||
aria-setsize={accountIds.length}
|
||||
>
|
||||
<AddedAccountItem
|
||||
accountId={accountId}
|
||||
onRemove={handleRemoveAccountItem}
|
||||
/>
|
||||
</Article>
|
||||
))}
|
||||
</ItemList>
|
||||
</Scrollable>
|
||||
</div>
|
||||
</FormStack>
|
||||
{!isEditMode && hasAccounts && (
|
||||
<div className={classes.stickyFooter}>
|
||||
<Button type='submit'>
|
||||
{id ? (
|
||||
<FormattedMessage id='lists.save' defaultMessage='Save' />
|
||||
) : (
|
||||
<FormattedMessage
|
||||
id='collections.continue'
|
||||
defaultMessage='Continue'
|
||||
/>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</form>
|
||||
|
||||
@ -36,7 +36,7 @@ import {
|
||||
import { useAppDispatch, useAppSelector } from 'mastodon/store';
|
||||
|
||||
import classes from './styles.module.scss';
|
||||
import { WizardStepHeader } from './wizard_step_header';
|
||||
import { WizardStepTitle } from './wizard_step_title';
|
||||
|
||||
export const CollectionDetails: React.FC = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
@ -152,7 +152,7 @@ export const CollectionDetails: React.FC = () => {
|
||||
<form onSubmit={handleSubmit} className={classes.form}>
|
||||
<FormStack className={classes.formFieldStack}>
|
||||
{!id && (
|
||||
<WizardStepHeader
|
||||
<WizardStepTitle
|
||||
step={2}
|
||||
title={
|
||||
<FormattedMessage
|
||||
|
||||
@ -44,7 +44,7 @@ export const messages = defineMessages({
|
||||
},
|
||||
});
|
||||
|
||||
function usePageTitle(id: string | undefined) {
|
||||
function usePageTitle(id: string | null) {
|
||||
const { path } = useRouteMatch();
|
||||
const location = useLocation();
|
||||
|
||||
@ -66,7 +66,7 @@ export const CollectionEditorPage: React.FC<{
|
||||
}> = ({ multiColumn }) => {
|
||||
const intl = useIntl();
|
||||
const dispatch = useAppDispatch();
|
||||
const { id } = useParams<{ id?: string }>();
|
||||
const { id = null } = useParams<{ id?: string }>();
|
||||
const { path } = useRouteMatch();
|
||||
const collection = useAppSelector((state) =>
|
||||
id ? state.collections.collections[id] : undefined,
|
||||
|
||||
@ -1,10 +1,17 @@
|
||||
.header {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.step {
|
||||
font-size: 13px;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 22px;
|
||||
font-size: 17px;
|
||||
font-weight: 500;
|
||||
line-height: 1.2;
|
||||
margin-top: 4px;
|
||||
}
|
||||
@ -14,79 +21,40 @@
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
/* Make form stretch full height of the column */
|
||||
.listHeading {
|
||||
margin-bottom: 8px;
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
/* Ensure sticky footer isn't covered by mobile bottom nav */
|
||||
.form {
|
||||
--bottom-spacing: 0;
|
||||
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 100%;
|
||||
padding-bottom: var(--bottom-spacing);
|
||||
padding-bottom: 0;
|
||||
|
||||
@media (width < 760px) {
|
||||
--bottom-spacing: var(--mobile-bottom-nav-height);
|
||||
}
|
||||
}
|
||||
|
||||
.selectedSuggestionIcon {
|
||||
box-sizing: border-box;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
margin-left: auto;
|
||||
padding: 2px;
|
||||
border-radius: 100%;
|
||||
color: var(--color-text-on-brand-base);
|
||||
background: var(--color-bg-brand-base);
|
||||
|
||||
[data-highlighted='true'] & {
|
||||
color: var(--color-bg-brand-base);
|
||||
background: var(--color-text-on-brand-base);
|
||||
}
|
||||
}
|
||||
|
||||
.formFieldStack {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.scrollableWrapper,
|
||||
.scrollableInner {
|
||||
margin-inline: -8px;
|
||||
}
|
||||
|
||||
.submitDisabledCallout {
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
.stickyFooter {
|
||||
position: sticky;
|
||||
bottom: var(--bottom-spacing);
|
||||
margin-top: -16px;
|
||||
padding: 16px;
|
||||
background-image: linear-gradient(
|
||||
to bottom,
|
||||
transparent,
|
||||
var(--color-bg-primary) 32px
|
||||
var(--color-bg-primary) 24px
|
||||
);
|
||||
}
|
||||
|
||||
.itemCountReadout {
|
||||
text-align: center;
|
||||
.formFieldStack {
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.actionWrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: min-content;
|
||||
min-width: 240px;
|
||||
margin-inline: auto;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.accountBadge {
|
||||
margin-inline-start: 56px;
|
||||
|
||||
@container (width < 360px) {
|
||||
margin-top: 4px;
|
||||
margin-inline-start: 46px;
|
||||
}
|
||||
.scrollableWrapper {
|
||||
margin-inline: -16px;
|
||||
}
|
||||
|
||||
@ -1,23 +0,0 @@
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
import classes from './styles.module.scss';
|
||||
|
||||
export const WizardStepHeader: React.FC<{
|
||||
step: number;
|
||||
title: React.ReactElement;
|
||||
description?: React.ReactElement;
|
||||
}> = ({ step, title, description }) => {
|
||||
return (
|
||||
<header>
|
||||
<FormattedMessage
|
||||
id='collections.create.steps'
|
||||
defaultMessage='Step {step}/{total}'
|
||||
values={{ step, total: 2 }}
|
||||
>
|
||||
{(content) => <p className={classes.step}>{content}</p>}
|
||||
</FormattedMessage>
|
||||
<h2 className={classes.title}>{title}</h2>
|
||||
{!!description && <p className={classes.description}>{description}</p>}
|
||||
</header>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,21 @@
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
import classes from './styles.module.scss';
|
||||
|
||||
export const WizardStepTitle: React.FC<{
|
||||
step: number;
|
||||
title: React.ReactElement;
|
||||
}> = ({ step, title }) => {
|
||||
return (
|
||||
<div>
|
||||
<p className={classes.step}>
|
||||
<FormattedMessage
|
||||
id='collections.create.steps'
|
||||
defaultMessage='Step {step}/{total}'
|
||||
values={{ step, total: 2 }}
|
||||
/>
|
||||
</p>
|
||||
<h2 className={classes.title}>{title}</h2>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@ -1,4 +1,4 @@
|
||||
import { useRef, useState } from 'react';
|
||||
import { useCallback, useRef, useState } from 'react';
|
||||
|
||||
import { useDebouncedCallback } from 'use-debounce';
|
||||
|
||||
@ -73,8 +73,13 @@ export function useSearchAccounts({
|
||||
{ leading: true, trailing: true },
|
||||
);
|
||||
|
||||
const resetAccounts = useCallback(() => {
|
||||
setAccountIds([]);
|
||||
}, []);
|
||||
|
||||
return {
|
||||
searchAccounts,
|
||||
resetAccounts,
|
||||
accountIds,
|
||||
isLoading: loadingState === 'loading',
|
||||
isError: loadingState === 'error',
|
||||
|
||||
@ -359,7 +359,8 @@
|
||||
"collection.share_template_other": "Check out this cool collection: {link}",
|
||||
"collection.share_template_own": "Check out my new collection: {link}",
|
||||
"collections.account_count": "{count, plural, one {# account} other {# accounts}}",
|
||||
"collections.accounts.empty_description": "Add up to {count} accounts you follow",
|
||||
"collections.accounts.empty_description": "Add up to {count} accounts",
|
||||
"collections.accounts.empty_editor_title": "No one is in this collection yet",
|
||||
"collections.accounts.empty_title": "This collection is empty",
|
||||
"collections.block_collection_owner": "Block account",
|
||||
"collections.by_account": "by {account_handle}",
|
||||
@ -373,7 +374,6 @@
|
||||
"collections.continue": "Continue",
|
||||
"collections.copy_link": "Copy link",
|
||||
"collections.copy_link_confirmation": "Copied collection link to clipboard",
|
||||
"collections.create.accounts_subtitle": "Only accounts you follow who have opted into discovery can be added.",
|
||||
"collections.create.accounts_title": "Who will you feature in this collection?",
|
||||
"collections.create.basic_details_title": "Basic details",
|
||||
"collections.create.steps": "Step {step}/{total}",
|
||||
@ -393,7 +393,7 @@
|
||||
"collections.error_loading_collections": "There was an error when trying to load your collections.",
|
||||
"collections.hidden_accounts_description": "You’ve blocked or muted {count, plural, one {this user} other {these users}}",
|
||||
"collections.hidden_accounts_link": "{count, plural, one {# hidden account} other {# hidden accounts}}",
|
||||
"collections.hints.accounts_counter": "{count} / {max} accounts",
|
||||
"collections.hints.accounts_counter": "{count}/{max} accounts",
|
||||
"collections.last_updated_at": "Last updated: {date}",
|
||||
"collections.manage_accounts": "Manage accounts",
|
||||
"collections.mark_as_sensitive": "Mark as sensitive",
|
||||
@ -401,13 +401,12 @@
|
||||
"collections.name_length_hint": "40 characters limit",
|
||||
"collections.new_collection": "New collection",
|
||||
"collections.no_collections_yet": "No collections yet.",
|
||||
"collections.old_last_post_note": "Last posted over a week ago",
|
||||
"collections.remove_account": "Remove this account",
|
||||
"collections.remove_account": "Remove",
|
||||
"collections.report_collection": "Report this collection",
|
||||
"collections.revoke_collection_inclusion": "Remove myself from this collection",
|
||||
"collections.revoke_inclusion.confirmation": "You've been removed from \"{collection}\"",
|
||||
"collections.revoke_inclusion.error": "There was an error, please try again later.",
|
||||
"collections.search_accounts_label": "Search for accounts to add…",
|
||||
"collections.search_accounts_label": "Search for an account to add",
|
||||
"collections.search_accounts_max_reached": "You have added the maximum number of accounts",
|
||||
"collections.sensitive": "Sensitive",
|
||||
"collections.share_short": "Share",
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user