[Glitch] Update design of Collections list page
Port 3d5cb624ba1bf861d41f4bf42f7ecf02b010718e to glitch-soc Signed-off-by: Claire <claire.github-309c@sitedethib.com>
This commit is contained in:
parent
44394878a4
commit
b08e2b8e60
@ -276,9 +276,11 @@ export const ColumnHeader: React.FC<Props> = ({
|
|||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const HeadingElement = hasTitle ? 'h1' : 'div';
|
||||||
|
|
||||||
const component = (
|
const component = (
|
||||||
<div className={wrapperClassName}>
|
<div className={wrapperClassName}>
|
||||||
<h1 className={buttonClassName}>
|
<HeadingElement className={buttonClassName}>
|
||||||
{hasTitle && (
|
{hasTitle && (
|
||||||
<>
|
<>
|
||||||
{backButton}
|
{backButton}
|
||||||
@ -311,7 +313,7 @@ export const ColumnHeader: React.FC<Props> = ({
|
|||||||
{extraButton}
|
{extraButton}
|
||||||
{collapseButton}
|
{collapseButton}
|
||||||
</div>
|
</div>
|
||||||
</h1>
|
</HeadingElement>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
className={collapsibleClassName}
|
className={collapsibleClassName}
|
||||||
|
|||||||
@ -1,8 +1,10 @@
|
|||||||
import type { ComponentPropsWithoutRef } from 'react';
|
import type { ComponentPropsWithoutRef } from 'react';
|
||||||
import { Children, forwardRef } from 'react';
|
import { forwardRef } from 'react';
|
||||||
|
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
|
|
||||||
|
import { hasReactChildren } from '@/flavours/glitch/utils/has_react_children';
|
||||||
|
|
||||||
import { LoadingIndicator } from '../loading_indicator';
|
import { LoadingIndicator } from '../loading_indicator';
|
||||||
|
|
||||||
export const Scrollable = forwardRef<
|
export const Scrollable = forwardRef<
|
||||||
@ -36,7 +38,7 @@ export const ItemList = forwardRef<
|
|||||||
emptyMessage?: React.ReactNode;
|
emptyMessage?: React.ReactNode;
|
||||||
}
|
}
|
||||||
>(({ isLoading, emptyMessage, className, children, ...otherProps }, ref) => {
|
>(({ isLoading, emptyMessage, className, children, ...otherProps }, ref) => {
|
||||||
if (!isLoading && Children.count(children) === 0 && emptyMessage) {
|
if (!isLoading && !hasReactChildren(children) && emptyMessage) {
|
||||||
return (
|
return (
|
||||||
<div className='empty-column-indicator'>
|
<div className='empty-column-indicator'>
|
||||||
<span>{emptyMessage}</span>
|
<span>{emptyMessage}</span>
|
||||||
|
|||||||
51
app/javascript/flavours/glitch/components/tab_list/index.tsx
Normal file
51
app/javascript/flavours/glitch/components/tab_list/index.tsx
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
import type { ComponentPropsWithoutRef, FC } from 'react';
|
||||||
|
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import type { NavLinkProps } from 'react-router-dom';
|
||||||
|
import { NavLink } from 'react-router-dom';
|
||||||
|
|
||||||
|
import classes from './styles.module.scss';
|
||||||
|
|
||||||
|
interface TabListProps {
|
||||||
|
/**
|
||||||
|
* Setting this will remove the default border and
|
||||||
|
* horizontal padding from the component
|
||||||
|
*/
|
||||||
|
plain?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Display a simple row of links as tabs.
|
||||||
|
* The current page will be highlighted automatically based on the link destination.
|
||||||
|
*/
|
||||||
|
export const TabList: FC<TabListProps & ComponentPropsWithoutRef<'div'>> = ({
|
||||||
|
plain,
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
...otherProps
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
{...otherProps}
|
||||||
|
className={classNames(
|
||||||
|
className,
|
||||||
|
classes.tabList,
|
||||||
|
!plain && classes.withSpaceAndBorder,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const TabLink: FC<NavLinkProps> = ({
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
...otherProps
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<NavLink className={classNames(classes.tab, className)} {...otherProps}>
|
||||||
|
{children}
|
||||||
|
</NavLink>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -0,0 +1,39 @@
|
|||||||
|
.tabList {
|
||||||
|
display: flex;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.withSpaceAndBorder {
|
||||||
|
padding-inline: 16px;
|
||||||
|
border-bottom: 1px solid var(--color-border-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab {
|
||||||
|
display: block;
|
||||||
|
padding: 18px 0;
|
||||||
|
color: var(--color-text-primary);
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 500;
|
||||||
|
text-decoration: none;
|
||||||
|
border-radius: 0;
|
||||||
|
transition: color 0.2s ease-in-out;
|
||||||
|
|
||||||
|
@container (width < 500px) {
|
||||||
|
flex: 1 1 0px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:not(:global(.active)):is(:hover, :focus) {
|
||||||
|
color: var(--color-text-brand-soft);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:global(.active) {
|
||||||
|
color: var(--color-text-brand);
|
||||||
|
border-bottom: 4px solid var(--color-border-brand);
|
||||||
|
padding-bottom: 14px;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,33 @@
|
|||||||
|
import type { Meta, StoryObj } from '@storybook/react-vite';
|
||||||
|
|
||||||
|
import { TabList, TabLink } from './index';
|
||||||
|
|
||||||
|
const meta = {
|
||||||
|
title: 'Components/TabList',
|
||||||
|
component: TabList,
|
||||||
|
subcomponents: { TabLink },
|
||||||
|
} satisfies Meta<typeof TabList>;
|
||||||
|
|
||||||
|
export default meta;
|
||||||
|
|
||||||
|
type Story = StoryObj<typeof meta>;
|
||||||
|
|
||||||
|
export const Default: Story = {
|
||||||
|
render: () => (
|
||||||
|
<TabList>
|
||||||
|
<TabLink to='/'>Activity</TabLink>
|
||||||
|
<TabLink to='/media'>Media</TabLink>
|
||||||
|
<TabLink to='/featured'>Featured</TabLink>
|
||||||
|
</TabList>
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Plain: Story = {
|
||||||
|
render: () => (
|
||||||
|
<TabList plain>
|
||||||
|
<TabLink to='/'>Activity</TabLink>
|
||||||
|
<TabLink to='/media'>Media</TabLink>
|
||||||
|
<TabLink to='/featured'>Featured</TabLink>
|
||||||
|
</TabList>
|
||||||
|
),
|
||||||
|
};
|
||||||
@ -355,48 +355,10 @@ $button-fallback-breakpoint: $button-breakpoint + 55px;
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.tabs,
|
|
||||||
.noTabs {
|
|
||||||
border-bottom: 1px solid var(--color-border-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.tabs {
|
|
||||||
display: flex;
|
|
||||||
gap: 16px;
|
|
||||||
padding: 0 16px;
|
|
||||||
|
|
||||||
@container (width < 500px) {
|
|
||||||
a {
|
|
||||||
flex: 1 1 0px;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
a {
|
|
||||||
display: block;
|
|
||||||
font-size: 15px;
|
|
||||||
font-weight: 500;
|
|
||||||
padding: 18px 0;
|
|
||||||
text-decoration: none;
|
|
||||||
color: var(--color-text-primary);
|
|
||||||
border-radius: 0;
|
|
||||||
transition: color 0.2s ease-in-out;
|
|
||||||
|
|
||||||
&:not([aria-current='page']):is(:hover, :focus) {
|
|
||||||
color: var(--color-text-brand-soft);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
:global(.active) {
|
|
||||||
color: var(--color-text-brand);
|
|
||||||
border-bottom: 4px solid var(--color-border-brand);
|
|
||||||
padding-bottom: 14px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.noTabs {
|
.noTabs {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
border-width: 0 0 1px;
|
border-width: 0 0 1px;
|
||||||
|
border-bottom: 1px solid var(--color-border-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.bannerBase {
|
.bannerBase {
|
||||||
|
|||||||
@ -3,8 +3,8 @@ import type { FC } from 'react';
|
|||||||
import { FormattedMessage } from 'react-intl';
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
|
||||||
import type { NavLinkProps } from 'react-router-dom';
|
import type { NavLinkProps } from 'react-router-dom';
|
||||||
import { NavLink } from 'react-router-dom';
|
|
||||||
|
|
||||||
|
import { TabLink, TabList } from '@/flavours/glitch/components/tab_list';
|
||||||
import { useAccount } from '@/flavours/glitch/hooks/useAccount';
|
import { useAccount } from '@/flavours/glitch/hooks/useAccount';
|
||||||
import { useAccountId } from '@/flavours/glitch/hooks/useAccountId';
|
import { useAccountId } from '@/flavours/glitch/hooks/useAccountId';
|
||||||
|
|
||||||
@ -28,20 +28,20 @@ export const AccountTabs: FC = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={classes.tabs}>
|
<TabList>
|
||||||
<NavLink isActive={isActive} to={`/@${acct}`}>
|
<TabLink isActive={isActive} to={`/@${acct}`}>
|
||||||
<FormattedMessage id='account.activity' defaultMessage='Activity' />
|
<FormattedMessage id='account.activity' defaultMessage='Activity' />
|
||||||
</NavLink>
|
</TabLink>
|
||||||
{show_media && (
|
{show_media && (
|
||||||
<NavLink exact to={`/@${acct}/media`}>
|
<TabLink exact to={`/@${acct}/media`}>
|
||||||
<FormattedMessage id='account.media' defaultMessage='Media' />
|
<FormattedMessage id='account.media' defaultMessage='Media' />
|
||||||
</NavLink>
|
</TabLink>
|
||||||
)}
|
)}
|
||||||
{show_featured && (
|
{show_featured && (
|
||||||
<NavLink exact to={`/@${acct}/featured`}>
|
<TabLink exact to={`/@${acct}/featured`}>
|
||||||
<FormattedMessage id='account.featured' defaultMessage='Featured' />
|
<FormattedMessage id='account.featured' defaultMessage='Featured' />
|
||||||
</NavLink>
|
</TabLink>
|
||||||
)}
|
)}
|
||||||
</div>
|
</TabList>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,6 +1,5 @@
|
|||||||
.wrapper {
|
.wrapper {
|
||||||
--list-item-padding: 16px;
|
--list-item-padding: 16px;
|
||||||
--list-item-padding-block: 12px;
|
|
||||||
--list-item-gap: 16px;
|
--list-item-gap: 16px;
|
||||||
|
|
||||||
&:not(.wrapperWithoutBorder) {
|
&:not(.wrapperWithoutBorder) {
|
||||||
|
|||||||
@ -144,7 +144,7 @@ export const CollectionEditorPage: React.FC<{
|
|||||||
/>
|
/>
|
||||||
</Switch>
|
</Switch>
|
||||||
) : (
|
) : (
|
||||||
<MaxCollectionsCallout />
|
<MaxCollectionsCallout className={classes.maxCollectionsError} />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -156,9 +156,11 @@ export const CollectionEditorPage: React.FC<{
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const MaxCollectionsCallout: React.FC = () => (
|
export const MaxCollectionsCallout: React.FC<{ className?: string }> = ({
|
||||||
|
className,
|
||||||
|
}) => (
|
||||||
<Callout
|
<Callout
|
||||||
className={classes.maxCollectionsError}
|
className={className}
|
||||||
title={
|
title={
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
id='collections.maximum_collection_count_reached'
|
id='collections.maximum_collection_count_reached'
|
||||||
|
|||||||
@ -5,9 +5,9 @@ import { defineMessages, useIntl, FormattedMessage } from 'react-intl';
|
|||||||
import { Helmet } from 'react-helmet';
|
import { Helmet } from 'react-helmet';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
|
import { EmptyState } from '@/flavours/glitch/components/empty_state';
|
||||||
|
import { TabLink, TabList } from '@/flavours/glitch/components/tab_list';
|
||||||
import AddIcon from '@/material-icons/400-24px/add.svg?react';
|
import AddIcon from '@/material-icons/400-24px/add.svg?react';
|
||||||
import CollectionsFilledIcon from '@/material-icons/400-24px/category-fill.svg?react';
|
|
||||||
import SquigglyArrow from '@/svg-icons/squiggly_arrow.svg?react';
|
|
||||||
import { Column } from 'flavours/glitch/components/column';
|
import { Column } from 'flavours/glitch/components/column';
|
||||||
import { ColumnHeader } from 'flavours/glitch/components/column_header';
|
import { ColumnHeader } from 'flavours/glitch/components/column_header';
|
||||||
import { DisplayNameSimple } from 'flavours/glitch/components/display_name/simple';
|
import { DisplayNameSimple } from 'flavours/glitch/components/display_name/simple';
|
||||||
@ -33,16 +33,39 @@ import {
|
|||||||
MaxCollectionsCallout,
|
MaxCollectionsCallout,
|
||||||
userCollectionLimit,
|
userCollectionLimit,
|
||||||
} from './editor';
|
} from './editor';
|
||||||
|
import classes from './styles.module.scss';
|
||||||
import { areCollectionsEnabled } from './utils';
|
import { areCollectionsEnabled } from './utils';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
headingMe: { id: 'column.my_collections', defaultMessage: 'My collections' },
|
headingMe: {
|
||||||
|
id: 'column.your_collections',
|
||||||
|
defaultMessage: 'Your Collections',
|
||||||
|
},
|
||||||
headingOther: {
|
headingOther: {
|
||||||
id: 'column.other_collections',
|
id: 'column.other_collections',
|
||||||
defaultMessage: 'Collections by {name}',
|
defaultMessage: "{name}'s Collections",
|
||||||
|
},
|
||||||
|
createdByYou: {
|
||||||
|
id: 'collections.list.created_by_you',
|
||||||
|
defaultMessage: 'Created by you',
|
||||||
|
},
|
||||||
|
createdByAuthor: {
|
||||||
|
id: 'collections.list.created_by_author',
|
||||||
|
defaultMessage: 'Created by {name}',
|
||||||
|
},
|
||||||
|
featuringYou: {
|
||||||
|
id: 'collections.list.featuring_you',
|
||||||
|
defaultMessage: 'Featuring you',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const CreateButton: React.FC = () => (
|
||||||
|
<Link to='/collections/new' className='button button--compact'>
|
||||||
|
<Icon id='plus' icon={AddIcon} />
|
||||||
|
<FormattedMessage {...editorMessages.newCollection} />
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
|
||||||
export function useAccountCollections(accountId: string | null | undefined) {
|
export function useAccountCollections(accountId: string | null | undefined) {
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
@ -65,33 +88,11 @@ export const Collections: React.FC<{
|
|||||||
|
|
||||||
const { collections, status } = useAccountCollections(accountId);
|
const { collections, status } = useAccountCollections(accountId);
|
||||||
|
|
||||||
const emptyMessage =
|
|
||||||
status === 'error' || !accountId ? (
|
|
||||||
<FormattedMessage
|
|
||||||
id='collections.error_loading_collections'
|
|
||||||
defaultMessage='There was an error when trying to load your collections.'
|
|
||||||
tagName='span'
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<span>
|
|
||||||
<FormattedMessage
|
|
||||||
id='collections.no_collections_yet'
|
|
||||||
defaultMessage='No collections yet.'
|
|
||||||
/>
|
|
||||||
<br />
|
|
||||||
<FormattedMessage
|
|
||||||
id='collections.create_a_collection_hint'
|
|
||||||
defaultMessage='Create a collection to recommend or share your favourite accounts with others.'
|
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<SquigglyArrow className='empty-column-indicator__arrow' />
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
|
|
||||||
const canCreateMoreCollections = collections.length < userCollectionLimit;
|
const canCreateMoreCollections = collections.length < userCollectionLimit;
|
||||||
const isOwnCollection = accountId === me;
|
const isOwnCollection = accountId === me;
|
||||||
|
const showCreateButton =
|
||||||
|
isOwnCollection && status === 'idle' && canCreateMoreCollections;
|
||||||
|
|
||||||
const titleMessage = isOwnCollection
|
const titleMessage = isOwnCollection
|
||||||
? messages.headingMe
|
? messages.headingMe
|
||||||
: messages.headingOther;
|
: messages.headingOther;
|
||||||
@ -103,45 +104,88 @@ export const Collections: React.FC<{
|
|||||||
name: <DisplayNameSimple account={account} />,
|
name: <DisplayNameSimple account={account} />,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const tabMessage = isOwnCollection
|
||||||
|
? messages.createdByYou
|
||||||
|
: messages.createdByAuthor;
|
||||||
|
|
||||||
|
const errorMessage = (status === 'error' || !accountId) && (
|
||||||
|
<FormattedMessage
|
||||||
|
id='collections.error_loading_collections'
|
||||||
|
defaultMessage='There was an error when trying to load your collections.'
|
||||||
|
tagName='span'
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Column bindToDocument={!multiColumn} label={pageTitle}>
|
<Column bindToDocument={!multiColumn} label={pageTitle}>
|
||||||
<ColumnHeader
|
<ColumnHeader showBackButton multiColumn={multiColumn} />
|
||||||
title={pageTitleHtml}
|
|
||||||
icon='collections'
|
|
||||||
iconComponent={CollectionsFilledIcon}
|
|
||||||
multiColumn={multiColumn}
|
|
||||||
extraButton={
|
|
||||||
isOwnCollection &&
|
|
||||||
status === 'idle' &&
|
|
||||||
canCreateMoreCollections && (
|
|
||||||
<Link
|
|
||||||
to='/collections/new'
|
|
||||||
className='column-header__button'
|
|
||||||
title={intl.formatMessage(editorMessages.create)}
|
|
||||||
aria-label={intl.formatMessage(editorMessages.create)}
|
|
||||||
>
|
|
||||||
<Icon id='plus' icon={AddIcon} />
|
|
||||||
</Link>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Scrollable>
|
<Scrollable>
|
||||||
{status === 'idle' && !canCreateMoreCollections && (
|
<header className={classes.header}>
|
||||||
<MaxCollectionsCallout />
|
<h1 className={classes.heading}>{pageTitleHtml}</h1>
|
||||||
|
<TabList plain>
|
||||||
|
<TabLink exact to={`/@${account?.acct}/collections`}>
|
||||||
|
{intl.formatMessage(tabMessage, {
|
||||||
|
name: <DisplayNameSimple account={account} />,
|
||||||
|
})}
|
||||||
|
</TabLink>
|
||||||
|
</TabList>
|
||||||
|
</header>
|
||||||
|
{collections.length > 0 ? (
|
||||||
|
<>
|
||||||
|
{status === 'idle' && (
|
||||||
|
<div className={classes.listHeader}>
|
||||||
|
<h2 className={classes.subHeading}>
|
||||||
|
<FormattedMessage
|
||||||
|
id='collections.list.collections_with_count'
|
||||||
|
defaultMessage='{count, plural, one {# Collection} other {# Collections}}'
|
||||||
|
values={{
|
||||||
|
count: collections.length,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</h2>
|
||||||
|
{showCreateButton && <CreateButton />}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<ItemList
|
||||||
|
emptyMessage={errorMessage}
|
||||||
|
isLoading={status === 'loading'}
|
||||||
|
>
|
||||||
|
{!canCreateMoreCollections && (
|
||||||
|
<MaxCollectionsCallout
|
||||||
|
className={classes.maxCollectionsError}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{collections.map((item, index) => (
|
||||||
|
<CollectionListItem
|
||||||
|
withTimestamp
|
||||||
|
withAuthorHandle={false}
|
||||||
|
key={item.id}
|
||||||
|
collection={item}
|
||||||
|
positionInList={index + 1}
|
||||||
|
listSize={collections.length}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</ItemList>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<EmptyState
|
||||||
|
title={
|
||||||
|
<FormattedMessage
|
||||||
|
id='empty_column.account_featured_self.showcase_accounts'
|
||||||
|
defaultMessage='Showcase your favorite accounts'
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
message={
|
||||||
|
<FormattedMessage
|
||||||
|
id='empty_column.account_featured_self.showcase_accounts_desc'
|
||||||
|
defaultMessage='Collections are curated lists of accounts to help others discover more of the Fediverse.'
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<CreateButton />
|
||||||
|
</EmptyState>
|
||||||
)}
|
)}
|
||||||
<ItemList emptyMessage={emptyMessage} isLoading={status === 'loading'}>
|
|
||||||
{collections.map((item, index) => (
|
|
||||||
<CollectionListItem
|
|
||||||
withTimestamp
|
|
||||||
withAuthorHandle={false}
|
|
||||||
key={item.id}
|
|
||||||
collection={item}
|
|
||||||
positionInList={index + 1}
|
|
||||||
listSize={collections.length}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</ItemList>
|
|
||||||
</Scrollable>
|
</Scrollable>
|
||||||
|
|
||||||
<Helmet>
|
<Helmet>
|
||||||
|
|||||||
@ -0,0 +1,35 @@
|
|||||||
|
.header {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
padding-top: 24px;
|
||||||
|
padding-inline: 16px;
|
||||||
|
border-bottom: 1px solid var(--color-border-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.heading {
|
||||||
|
font-size: 22px;
|
||||||
|
font-weight: 500;
|
||||||
|
line-height: 1.2;
|
||||||
|
overflow-wrap: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.listHeader {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
min-height: 28px;
|
||||||
|
padding-top: 16px;
|
||||||
|
padding-inline: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subHeading {
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 500;
|
||||||
|
line-height: 1.2;
|
||||||
|
overflow-wrap: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.maxCollectionsError {
|
||||||
|
margin: 8px 16px;
|
||||||
|
}
|
||||||
12
app/javascript/flavours/glitch/utils/has_react_children.ts
Normal file
12
app/javascript/flavours/glitch/utils/has_react_children.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import { Children } from 'react';
|
||||||
|
import type { ReactNode } from 'react';
|
||||||
|
|
||||||
|
export function hasReactChildren(children: ReactNode): boolean {
|
||||||
|
if (!children) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const childrenCount = Children.toArray(children).filter(Boolean).length;
|
||||||
|
|
||||||
|
return childrenCount > 0;
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user