Remove 'radio-button.scss', 'dirctory.scss' and move RadioButton to UI Components
This commit is contained in:
parent
5f67eb26ca
commit
9b70a2487e
9 changed files with 149 additions and 309 deletions
|
@ -1,28 +0,0 @@
|
|||
import classNames from 'clsx';
|
||||
import React from 'react';
|
||||
|
||||
interface IRadioButton {
|
||||
value: string,
|
||||
checked?: boolean,
|
||||
name: string,
|
||||
onChange: React.ChangeEventHandler<HTMLInputElement>,
|
||||
label: React.ReactNode,
|
||||
}
|
||||
|
||||
const RadioButton: React.FC<IRadioButton> = ({ name, value, checked, onChange, label }) => (
|
||||
<label className='radio-button'>
|
||||
<input
|
||||
name={name}
|
||||
type='radio'
|
||||
value={value}
|
||||
checked={checked}
|
||||
onChange={onChange}
|
||||
/>
|
||||
|
||||
<span className={classNames('radio-button__input', { checked })} />
|
||||
|
||||
<span>{label}</span>
|
||||
</label>
|
||||
);
|
||||
|
||||
export default RadioButton;
|
|
@ -31,6 +31,7 @@ export {
|
|||
export { default as Modal } from './modal/modal';
|
||||
export { default as PhoneInput } from './phone-input/phone-input';
|
||||
export { default as ProgressBar } from './progress-bar/progress-bar';
|
||||
export { default as RadioButton } from './radio-button/radio-button';
|
||||
export { default as Select } from './select/select';
|
||||
export { default as Spinner } from './spinner/spinner';
|
||||
export { default as Stack } from './stack/stack';
|
||||
|
|
37
app/soapbox/components/ui/radio-button/radio-button.tsx
Normal file
37
app/soapbox/components/ui/radio-button/radio-button.tsx
Normal file
|
@ -0,0 +1,37 @@
|
|||
import React, { useMemo } from 'react';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
interface IRadioButton {
|
||||
value: string
|
||||
checked?: boolean
|
||||
name: string
|
||||
onChange: React.ChangeEventHandler<HTMLInputElement>
|
||||
label: React.ReactNode
|
||||
}
|
||||
|
||||
/**
|
||||
* A group for radio input with label.
|
||||
*/
|
||||
const RadioButton: React.FC<IRadioButton> = ({ name, value, checked, onChange, label }) => {
|
||||
const formFieldId: string = useMemo(() => `radio-${uuidv4()}`, []);
|
||||
|
||||
return (
|
||||
<div className='flex items-center'>
|
||||
<input
|
||||
type='radio'
|
||||
name={name}
|
||||
id={formFieldId}
|
||||
value={value}
|
||||
checked={checked}
|
||||
onChange={onChange}
|
||||
className='h-4 w-4 border-gray-300 text-primary-600 focus:ring-primary-500'
|
||||
/>
|
||||
|
||||
<label htmlFor={formFieldId} className='ml-3 block text-sm font-medium text-gray-700'>
|
||||
{label}
|
||||
</label>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default RadioButton;
|
|
@ -1,13 +1,12 @@
|
|||
import classNames from 'clsx';
|
||||
import React from 'react';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import { getSettings } from 'soapbox/actions/settings';
|
||||
import Avatar from 'soapbox/components/avatar';
|
||||
import DisplayName from 'soapbox/components/display-name';
|
||||
import Badge from 'soapbox/components/badge';
|
||||
import RelativeTimestamp from 'soapbox/components/relative-timestamp';
|
||||
import { Text } from 'soapbox/components/ui';
|
||||
import { Stack, Text } from 'soapbox/components/ui';
|
||||
import AccountContainer from 'soapbox/containers/account-container';
|
||||
import ActionButton from 'soapbox/features/ui/components/action-button';
|
||||
import { useAppSelector } from 'soapbox/hooks';
|
||||
import { makeGetAccount } from 'soapbox/selectors';
|
||||
|
@ -29,51 +28,76 @@ const AccountCard: React.FC<IAccountCard> = ({ id }) => {
|
|||
const followedBy = me !== account.id && account.relationship?.followed_by;
|
||||
|
||||
return (
|
||||
<div className='directory__card'>
|
||||
{followedBy &&
|
||||
<div className='directory__card__info'>
|
||||
<span className='relationship-tag'>
|
||||
<FormattedMessage id='account.follows_you' defaultMessage='Follows you' />
|
||||
</span>
|
||||
</div>}
|
||||
<div className='directory__card__action-button'>
|
||||
<ActionButton account={account} small />
|
||||
</div>
|
||||
<div className='directory__card__img'>
|
||||
<img src={autoPlayGif ? account.header : account.header_static} alt='' className='parallax' />
|
||||
</div>
|
||||
<div className='flex flex-col divide-y divide-gray-200 dark:divide-primary-700 rounded-lg bg-white dark:bg-primary-800 text-center shadow'>
|
||||
<div className='relative'>
|
||||
{followedBy && (
|
||||
<div className='absolute top-2.5 left-2.5'>
|
||||
<Badge
|
||||
slug='opaque'
|
||||
title={<FormattedMessage id='account.follows_you' defaultMessage='Follows you' />}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className='directory__card__bar'>
|
||||
<Link className='directory__card__bar__name' to={`/@${account.acct}`}>
|
||||
<Avatar account={account} size={48} />
|
||||
<DisplayName account={account} />
|
||||
</Link>
|
||||
</div>
|
||||
<div className='absolute bottom-2.5 right-2.5'>
|
||||
<ActionButton account={account} small />
|
||||
</div>
|
||||
|
||||
<div className='directory__card__extra'>
|
||||
<Text
|
||||
className={classNames('account__header__content', (account.note.length === 0 || account.note === '<p></p>') && 'empty')}
|
||||
dangerouslySetInnerHTML={{ __html: account.note_emojified }}
|
||||
<img
|
||||
src={autoPlayGif ? account.header : account.header_static}
|
||||
alt=''
|
||||
className='object-cover h-32 w-full rounded-t-lg'
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className='directory__card__extra'>
|
||||
<div className='accounts-table__count'>
|
||||
<Text theme='primary' size='sm'>
|
||||
<Stack space={4} className='p-3'>
|
||||
<AccountContainer
|
||||
id={account.id}
|
||||
withRelationship={false}
|
||||
/>
|
||||
|
||||
<Text
|
||||
truncate
|
||||
align='left'
|
||||
className={classNames('[&_br]:hidden [&_p]:hidden [&_p:first-child]:inline [&_p:first-child]:truncate')}
|
||||
dangerouslySetInnerHTML={{ __html: account.note_emojified || ' ' }}
|
||||
/>
|
||||
</Stack>
|
||||
|
||||
<div className='grid grid-cols-3 gap-1 py-4'>
|
||||
<Stack>
|
||||
<Text theme='primary' size='md' weight='medium'>
|
||||
{shortNumberFormat(account.statuses_count)}
|
||||
</Text> <small><FormattedMessage id='account.posts' defaultMessage='Posts' /></small>
|
||||
</div>
|
||||
<div className='accounts-table__count'>
|
||||
<Text theme='primary' size='sm'>
|
||||
</Text>
|
||||
|
||||
<Text theme='muted' size='sm'>
|
||||
<FormattedMessage id='account.posts' defaultMessage='Posts' />
|
||||
</Text>
|
||||
</Stack>
|
||||
|
||||
<Stack>
|
||||
<Text theme='primary' size='md' weight='medium'>
|
||||
{shortNumberFormat(account.followers_count)}
|
||||
</Text> <small><FormattedMessage id='account.followers' defaultMessage='Followers' />
|
||||
</small>
|
||||
</div>
|
||||
<div className='accounts-table__count'>
|
||||
{account.last_status_at === null
|
||||
? <Text theme='primary' size='sm'><FormattedMessage id='account.never_active' defaultMessage='Never' /></Text>
|
||||
: <RelativeTimestamp className='text-primary-600 dark:text-primary-400' timestamp={account.last_status_at} />} <small><FormattedMessage id='account.last_status' defaultMessage='Last active' /></small>
|
||||
</div>
|
||||
</Text>
|
||||
|
||||
<Text theme='muted' size='sm'>
|
||||
<FormattedMessage id='account.followers' defaultMessage='Followers' />
|
||||
</Text>
|
||||
</Stack>
|
||||
|
||||
<Stack>
|
||||
<Text theme='primary' size='md' weight='medium'>
|
||||
{account.last_status_at === null ? (
|
||||
<FormattedMessage id='account.never_active' defaultMessage='Never' />
|
||||
) : (
|
||||
<RelativeTimestamp theme='inherit' timestamp={account.last_status_at} />
|
||||
)}
|
||||
</Text>
|
||||
|
||||
<Text theme='muted' size='sm'>
|
||||
<FormattedMessage id='account.last_status' defaultMessage='Last active' />
|
||||
</Text>
|
||||
</Stack>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -6,8 +6,7 @@ import { useLocation } from 'react-router-dom';
|
|||
|
||||
import { fetchDirectory, expandDirectory } from 'soapbox/actions/directory';
|
||||
import LoadMore from 'soapbox/components/load-more';
|
||||
import RadioButton from 'soapbox/components/radio-button';
|
||||
import Column from 'soapbox/features/ui/components/column';
|
||||
import { Column, RadioButton, Stack, Text } from 'soapbox/components/ui';
|
||||
import { useAppSelector } from 'soapbox/hooks';
|
||||
import { getFeatures } from 'soapbox/utils/features';
|
||||
|
||||
|
@ -52,26 +51,49 @@ const Directory = () => {
|
|||
};
|
||||
|
||||
return (
|
||||
<Column icon='address-book-o' label={intl.formatMessage(messages.title)}>
|
||||
<div className='directory__filter-form'>
|
||||
<div className='directory__filter-form__column' role='group'>
|
||||
<RadioButton name='order' value='active' label={intl.formatMessage(messages.recentlyActive)} checked={order === 'active'} onChange={handleChangeOrder} />
|
||||
<RadioButton name='order' value='new' label={intl.formatMessage(messages.newArrivals)} checked={order === 'new'} onChange={handleChangeOrder} />
|
||||
<Column label={intl.formatMessage(messages.title)}>
|
||||
<Stack space={4}>
|
||||
<div className='grid grid-cols-2 gap-2'>
|
||||
<div>
|
||||
<Text weight='medium'>Display filter</Text>
|
||||
<fieldset className='mt-3'>
|
||||
<legend className='sr-only'>Display filter</legend>
|
||||
<div className='space-y-2'>
|
||||
<RadioButton name='order' value='active' label={intl.formatMessage(messages.recentlyActive)} checked={order === 'active'} onChange={handleChangeOrder} />
|
||||
<RadioButton name='order' value='new' label={intl.formatMessage(messages.newArrivals)} checked={order === 'new'} onChange={handleChangeOrder} />
|
||||
</div>
|
||||
</fieldset>
|
||||
</div>
|
||||
|
||||
{features.federating && (
|
||||
<div>
|
||||
<Text weight='medium'>Fediverse filter</Text>
|
||||
<fieldset className='mt-3'>
|
||||
<legend className='sr-only'>Fediverse filter</legend>
|
||||
<div className='space-y-2'>
|
||||
<RadioButton name='local' value='1' label={intl.formatMessage(messages.local, { domain: title })} checked={local} onChange={handleChangeLocal} />
|
||||
<RadioButton name='local' value='0' label={intl.formatMessage(messages.federated)} checked={!local} onChange={handleChangeLocal} />
|
||||
</div>
|
||||
</fieldset>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{features.federating && (
|
||||
<div className='directory__filter-form__column' role='group'>
|
||||
<RadioButton name='local' value='1' label={intl.formatMessage(messages.local, { domain: title })} checked={local} onChange={handleChangeLocal} />
|
||||
<RadioButton name='local' value='0' label={intl.formatMessage(messages.federated)} checked={!local} onChange={handleChangeLocal} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div
|
||||
className={
|
||||
classNames({
|
||||
'grid grid-cols-1 sm:grid-cols-2 gap-2.5': true,
|
||||
'opacity-30': isLoading,
|
||||
})
|
||||
}
|
||||
>
|
||||
{accountIds.map((accountId) => (
|
||||
<AccountCard id={accountId} key={accountId} />),
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className={classNames('directory__list', { loading: isLoading })}>
|
||||
{accountIds.map((accountId) => <AccountCard id={accountId} key={accountId} />)}
|
||||
</div>
|
||||
|
||||
<LoadMore onClick={handleLoadMore} visible={!isLoading} />
|
||||
<LoadMore onClick={handleLoadMore} disabled={isLoading} />
|
||||
</Stack>
|
||||
</Column>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -97,7 +97,10 @@ const normalizeList = (state: State, path: NestedListPath | ListPath, accounts:
|
|||
|
||||
const appendToList = (state: State, path: NestedListPath | ListPath, accounts: APIEntity[], next: string | null) => {
|
||||
return state.updateIn(path, map => {
|
||||
return (map as List).set('next', next).update('items', list => (list as Items).concat(accounts.map(item => item.id)));
|
||||
return (map as List)
|
||||
.set('next', next)
|
||||
.set('isLoading', false)
|
||||
.update('items', list => (list as Items).concat(accounts.map(item => item.id)));
|
||||
});
|
||||
};
|
||||
|
||||
|
|
|
@ -59,8 +59,6 @@
|
|||
@import 'components/crypto-donate';
|
||||
@import 'components/aliases';
|
||||
@import 'components/icon';
|
||||
@import 'components/radio-button';
|
||||
@import 'components/directory';
|
||||
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
|
|
|
@ -1,179 +0,0 @@
|
|||
.directory {
|
||||
&__filter-form {
|
||||
display: flex;
|
||||
gap: 30px;
|
||||
background: var(--foreground-color);
|
||||
|
||||
&__column {
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
.radio-button {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
&__list {
|
||||
display: grid;
|
||||
grid-gap: 10px;
|
||||
grid-template-columns: minmax(0, 50%) minmax(0, 50%);
|
||||
width: 100%;
|
||||
transition: opacity 100ms ease-in;
|
||||
box-sizing: border-box;
|
||||
|
||||
&.loading {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 630px) {
|
||||
grid-template-columns: minmax(0, 100%);
|
||||
}
|
||||
}
|
||||
|
||||
&__card {
|
||||
@apply rounded-lg bg-gray-100 dark:bg-primary-800;
|
||||
box-sizing: border-box;
|
||||
margin-bottom: 0;
|
||||
box-shadow: 0 0 6px 0 rgba(0, 0, 0, 0.1);
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
|
||||
&__info {
|
||||
z-index: 1;
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
left: 10px;
|
||||
}
|
||||
|
||||
&__action-button {
|
||||
z-index: 1;
|
||||
position: absolute;
|
||||
top: 78px;
|
||||
right: 12px;
|
||||
}
|
||||
|
||||
&__img {
|
||||
@apply bg-primary-200 dark:bg-gray-600;
|
||||
height: 125px;
|
||||
position: relative;
|
||||
|
||||
img {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
object-fit: cover;
|
||||
}
|
||||
}
|
||||
|
||||
&__bar {
|
||||
@apply bg-primary-200 dark:bg-primary-700;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 10px;
|
||||
|
||||
&__name {
|
||||
flex: 1 1 auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
text-decoration: none;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.account__avatar {
|
||||
flex: 0 0 auto;
|
||||
width: 48px;
|
||||
min-width: 48px;
|
||||
height: 48px;
|
||||
padding-top: 2px;
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
margin: 0;
|
||||
border-radius: 4px;
|
||||
background: var(--brand-color--faint);
|
||||
object-fit: cover;
|
||||
}
|
||||
}
|
||||
|
||||
.display-name {
|
||||
margin-left: 15px;
|
||||
text-align: left;
|
||||
|
||||
strong {
|
||||
@apply text-black dark:text-white;
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
span:not(.verified-icon) {
|
||||
@apply text-gray-500 dark:text-gray-400;
|
||||
display: block;
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.verified-icon div,
|
||||
.verified-icon svg {
|
||||
@apply inline;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__extra {
|
||||
background: var(--foreground-color);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
.accounts-table__count {
|
||||
padding: 15px 0;
|
||||
text-align: center;
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
width: 33.33%;
|
||||
flex: 0 0 auto;
|
||||
|
||||
small {
|
||||
display: block;
|
||||
color: var(--primary-text-color--faint);
|
||||
font-weight: 400;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
.account__header__content {
|
||||
@apply border-b border-solid border-gray-200 dark:border-primary-500;
|
||||
box-sizing: border-box;
|
||||
padding: 15px 10px;
|
||||
width: 100%;
|
||||
min-height: 50px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
|
||||
&.empty {
|
||||
border-color: transparent;
|
||||
}
|
||||
|
||||
p {
|
||||
display: none;
|
||||
|
||||
&:first-child {
|
||||
display: inline;
|
||||
}
|
||||
}
|
||||
|
||||
br {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,38 +0,0 @@
|
|||
.radio-button {
|
||||
@apply text-black dark:text-white;
|
||||
font-size: 14px;
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
padding: 6px 0;
|
||||
line-height: 18px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
cursor: pointer;
|
||||
|
||||
input[type=radio],
|
||||
input[type=checkbox] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&__input {
|
||||
@apply inline-block relative box-border border border-solid border-primary-600;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
flex: 0 0 auto;
|
||||
margin-right: 10px;
|
||||
top: -1px;
|
||||
border-radius: 50%;
|
||||
vertical-align: middle;
|
||||
|
||||
&.checked {
|
||||
@apply bg-primary-600;
|
||||
}
|
||||
|
||||
&:active,
|
||||
&:focus,
|
||||
&:hover {
|
||||
@apply border-4;
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue