Restore Patron features, context to TSX
This commit is contained in:
parent
57e5d81e33
commit
3f9cc3cd04
14 changed files with 184 additions and 154 deletions
17
app/soapbox/__fixtures__/patron-instance.json
Normal file
17
app/soapbox/__fixtures__/patron-instance.json
Normal file
|
@ -0,0 +1,17 @@
|
|||
{
|
||||
"funding": {
|
||||
"amount": 3500,
|
||||
"patrons": 3,
|
||||
"currency": "usd",
|
||||
"interval": "monthly"
|
||||
},
|
||||
"goals": [
|
||||
{
|
||||
"amount": 20000,
|
||||
"currency": "usd",
|
||||
"interval": "monthly",
|
||||
"text": "I'll be able to afford an avocado."
|
||||
}
|
||||
],
|
||||
"url": "https://patron.gleasonator.com"
|
||||
}
|
4
app/soapbox/__fixtures__/patron-user.json
Normal file
4
app/soapbox/__fixtures__/patron-user.json
Normal file
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"is_patron": true,
|
||||
"url": "https://gleasonator.com/users/dave"
|
||||
}
|
|
@ -1,13 +1,25 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
import React from 'react';
|
||||
|
||||
const Badge = (props: any) => (
|
||||
<span data-testid='badge' className={'badge badge--' + props.slug}>{props.title}</span>
|
||||
interface IBadge {
|
||||
title: string,
|
||||
slug: 'patron' | 'admin' | 'moderator' | 'bot' | 'opaque',
|
||||
}
|
||||
|
||||
/** Badge to display on a user's profile. */
|
||||
const Badge: React.FC<IBadge> = ({ title, slug }) => (
|
||||
<span
|
||||
data-testid='badge'
|
||||
className={classNames('inline-flex items-center px-2 py-0.5 rounded text-xs font-medium text-white', {
|
||||
'bg-fuchsia-700': slug === 'patron',
|
||||
'bg-black': slug === 'admin',
|
||||
'bg-cyan-600': slug === 'moderator',
|
||||
'bg-gray-100 text-gray-800': slug === 'bot',
|
||||
'bg-white bg-opacity-75 text-gray-900': slug === 'opaque',
|
||||
})}
|
||||
>
|
||||
{title}
|
||||
</span>
|
||||
);
|
||||
|
||||
Badge.propTypes = {
|
||||
title: PropTypes.string.isRequired,
|
||||
slug: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default Badge;
|
||||
|
|
|
@ -5,8 +5,8 @@ interface IProgressBar {
|
|||
}
|
||||
|
||||
const ProgressBar: React.FC<IProgressBar> = ({ progress }) => (
|
||||
<div className='progress-bar'>
|
||||
<div className='progress-bar__progress' style={{ width: `${Math.floor(progress*100)}%` }} />
|
||||
<div className='h-2 w-full rounded-md bg-gray-300 dark:bg-slate-700 overflow-hidden'>
|
||||
<div className='h-full bg-primary-500' style={{ width: `${Math.floor(progress*100)}%` }} />
|
||||
</div>
|
||||
);
|
||||
|
||||
|
|
|
@ -1,78 +0,0 @@
|
|||
import { Map as ImmutableMap } from 'immutable';
|
||||
import React from 'react';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import { injectIntl } from 'react-intl';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { fetchPatronInstance } from 'soapbox/actions/patron';
|
||||
import Icon from 'soapbox/components/icon';
|
||||
|
||||
import ProgressBar from '../../../components/progress_bar';
|
||||
|
||||
const moneyFormat = amount => (
|
||||
new Intl
|
||||
.NumberFormat('en-US', {
|
||||
style: 'currency',
|
||||
currency: 'usd',
|
||||
notation: 'compact',
|
||||
})
|
||||
.format(amount/100)
|
||||
);
|
||||
|
||||
class FundingPanel extends ImmutablePureComponent {
|
||||
|
||||
componentDidMount() {
|
||||
this.props.dispatch(fetchPatronInstance());
|
||||
}
|
||||
|
||||
render() {
|
||||
const { patron } = this.props;
|
||||
if (patron.isEmpty()) return null;
|
||||
|
||||
const amount = patron.getIn(['funding', 'amount']);
|
||||
const goal = patron.getIn(['goals', '0', 'amount']);
|
||||
const goal_text = patron.getIn(['goals', '0', 'text']);
|
||||
const goal_reached = amount >= goal;
|
||||
let ratio_text;
|
||||
|
||||
if (goal_reached) {
|
||||
ratio_text = <><strong>{moneyFormat(goal)}</strong> per month<span className='funding-panel__reached'>— reached!</span></>;
|
||||
} else {
|
||||
ratio_text = <><strong>{moneyFormat(amount)} out of {moneyFormat(goal)}</strong> per month</>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='wtf-panel funding-panel'>
|
||||
<div className='wtf-panel-header'>
|
||||
<Icon src={require('@tabler/icons/icons/chart-line.svg')} className='wtf-panel-header__icon' />
|
||||
<span className='wtf-panel-header__label'>
|
||||
<span>Funding Goal</span>
|
||||
</span>
|
||||
</div>
|
||||
<div className='wtf-panel__content'>
|
||||
<div className='funding-panel__ratio'>
|
||||
{ratio_text}
|
||||
</div>
|
||||
<ProgressBar progress={amount/goal} />
|
||||
<div className='funding-panel__description'>
|
||||
{goal_text}
|
||||
</div>
|
||||
<a className='button' href={patron.get('url')}>Donate</a>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
const mapStateToProps = state => {
|
||||
return {
|
||||
patron: state.getIn(['patron', 'instance'], ImmutableMap()),
|
||||
};
|
||||
};
|
||||
|
||||
export default injectIntl(
|
||||
connect(mapStateToProps, null, null, {
|
||||
forwardRef: true,
|
||||
},
|
||||
)(FundingPanel));
|
79
app/soapbox/features/ui/components/funding_panel.tsx
Normal file
79
app/soapbox/features/ui/components/funding_panel.tsx
Normal file
|
@ -0,0 +1,79 @@
|
|||
import React, { useEffect } from 'react';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
|
||||
import { fetchPatronInstance } from 'soapbox/actions/patron';
|
||||
import { Widget, Button, Text } from 'soapbox/components/ui';
|
||||
import { useAppSelector, useAppDispatch } from 'soapbox/hooks';
|
||||
|
||||
import ProgressBar from '../../../components/progress_bar';
|
||||
|
||||
/** Open link in a new tab. */
|
||||
// https://stackoverflow.com/a/28374344/8811886
|
||||
const openInNewTab = (href: string): void => {
|
||||
Object.assign(document.createElement('a'), {
|
||||
target: '_blank',
|
||||
href: href,
|
||||
}).click();
|
||||
};
|
||||
|
||||
/** Formats integer to USD string. */
|
||||
const moneyFormat = (amount: number): string => (
|
||||
new Intl
|
||||
.NumberFormat('en-US', {
|
||||
style: 'currency',
|
||||
currency: 'usd',
|
||||
notation: 'compact',
|
||||
})
|
||||
.format(amount/100)
|
||||
);
|
||||
|
||||
const FundingPanel: React.FC = () => {
|
||||
const history = useHistory();
|
||||
const dispatch = useAppDispatch();
|
||||
const patron = useAppSelector(state => state.patron.instance);
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(fetchPatronInstance());
|
||||
}, []);
|
||||
|
||||
if (patron.funding.isEmpty() || patron.goals.isEmpty()) return null;
|
||||
|
||||
const amount = patron.getIn(['funding', 'amount']) as number;
|
||||
const goal = patron.getIn(['goals', '0', 'amount']) as number;
|
||||
const goalText = patron.getIn(['goals', '0', 'text']) as string;
|
||||
const goalReached = amount >= goal;
|
||||
let ratioText;
|
||||
|
||||
if (goalReached) {
|
||||
ratioText = <><strong>{moneyFormat(goal)}</strong> per month<span className='funding-panel__reached'>— reached!</span></>;
|
||||
} else {
|
||||
ratioText = <><strong>{moneyFormat(amount)} out of {moneyFormat(goal)}</strong> per month</>;
|
||||
}
|
||||
|
||||
const handleDonateClick = () => {
|
||||
openInNewTab(patron.url);
|
||||
};
|
||||
|
||||
return (
|
||||
<Widget
|
||||
title={<FormattedMessage id='patron.title' defaultMessage='Funding Goal' />}
|
||||
onActionClick={handleDonateClick}
|
||||
>
|
||||
<div className='funding-panel__ratio'>
|
||||
<Text>{ratioText}</Text>
|
||||
</div>
|
||||
<ProgressBar progress={amount/goal} />
|
||||
<div className='funding-panel__description'>
|
||||
<Text>{goalText}</Text>
|
||||
</div>
|
||||
<div>
|
||||
<Button theme='ghost' onClick={handleDonateClick}>
|
||||
<FormattedMessage id='patron.donate' defaultMessage='Donate' />
|
||||
</Button>
|
||||
</div>
|
||||
</Widget>
|
||||
);
|
||||
};
|
||||
|
||||
export default FundingPanel;
|
|
@ -16,6 +16,7 @@ import { normalizeEmoji } from 'soapbox/normalizers/emoji';
|
|||
import { unescapeHTML } from 'soapbox/utils/html';
|
||||
import { mergeDefined, makeEmojiMap } from 'soapbox/utils/normalizers';
|
||||
|
||||
import type { PatronAccount } from 'soapbox/reducers/patron';
|
||||
import type { Emoji, Field, EmbeddedEntity } from 'soapbox/types/entities';
|
||||
|
||||
// https://docs.joinmastodon.org/entities/account/
|
||||
|
@ -57,7 +58,7 @@ export const AccountRecord = ImmutableRecord({
|
|||
moderator: false,
|
||||
note_emojified: '',
|
||||
note_plain: '',
|
||||
patron: ImmutableMap<string, any>(),
|
||||
patron: null as PatronAccount | null,
|
||||
relationship: ImmutableList<ImmutableMap<string, any>>(),
|
||||
should_refetch: false,
|
||||
staff: false,
|
||||
|
|
|
@ -11,6 +11,7 @@ import {
|
|||
TrendsPanel,
|
||||
SignUpPanel,
|
||||
PromoPanel,
|
||||
FundingPanel,
|
||||
CryptoDonatePanel,
|
||||
BirthdayPanel,
|
||||
} from 'soapbox/features/ui/util/async-components';
|
||||
|
@ -33,7 +34,7 @@ const mapStateToProps = state => {
|
|||
return {
|
||||
me,
|
||||
account: state.getIn(['accounts', me]),
|
||||
showFundingPanel: hasPatron,
|
||||
hasPatron,
|
||||
hasCrypto,
|
||||
cryptoLimit,
|
||||
features,
|
||||
|
@ -49,7 +50,7 @@ class HomePage extends ImmutablePureComponent {
|
|||
}
|
||||
|
||||
render() {
|
||||
const { me, children, account, features, hasCrypto, cryptoLimit } = this.props;
|
||||
const { me, children, account, features, hasPatron, hasCrypto, cryptoLimit } = this.props;
|
||||
|
||||
const acct = account ? account.get('acct') : '';
|
||||
|
||||
|
@ -90,6 +91,11 @@ class HomePage extends ImmutablePureComponent {
|
|||
{Component => <Component limit={3} />}
|
||||
</BundleContainer>
|
||||
)}
|
||||
{hasPatron && (
|
||||
<BundleContainer fetchComponent={FundingPanel}>
|
||||
{Component => <Component />}
|
||||
</BundleContainer>
|
||||
)}
|
||||
{hasCrypto && cryptoLimit > 0 && (
|
||||
<BundleContainer fetchComponent={CryptoDonatePanel}>
|
||||
{Component => <Component limit={cryptoLimit} />}
|
||||
|
|
|
@ -1,24 +0,0 @@
|
|||
import { Map as ImmutableMap, fromJS } from 'immutable';
|
||||
|
||||
import {
|
||||
PATRON_INSTANCE_FETCH_SUCCESS,
|
||||
PATRON_ACCOUNT_FETCH_SUCCESS,
|
||||
} from '../actions/patron';
|
||||
|
||||
const initialState = ImmutableMap();
|
||||
|
||||
const normalizePatronAccount = (state, account) => {
|
||||
const normalized = fromJS(account).deleteAll(['url']);
|
||||
return state.setIn(['accounts', account.url], normalized);
|
||||
};
|
||||
|
||||
export default function patron(state = initialState, action) {
|
||||
switch(action.type) {
|
||||
case PATRON_INSTANCE_FETCH_SUCCESS:
|
||||
return state.set('instance', fromJS(action.instance));
|
||||
case PATRON_ACCOUNT_FETCH_SUCCESS:
|
||||
return normalizePatronAccount(state, action.account);
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
50
app/soapbox/reducers/patron.ts
Normal file
50
app/soapbox/reducers/patron.ts
Normal file
|
@ -0,0 +1,50 @@
|
|||
import {
|
||||
Map as ImmutableMap,
|
||||
List as ImmutableList,
|
||||
Record as ImmutableRecord,
|
||||
fromJS,
|
||||
} from 'immutable';
|
||||
|
||||
import {
|
||||
PATRON_INSTANCE_FETCH_SUCCESS,
|
||||
PATRON_ACCOUNT_FETCH_SUCCESS,
|
||||
} from '../actions/patron';
|
||||
|
||||
import type { AnyAction } from 'redux';
|
||||
|
||||
const PatronAccountRecord = ImmutableRecord({
|
||||
is_patron: false,
|
||||
url: '',
|
||||
});
|
||||
|
||||
const PatronInstanceRecord = ImmutableRecord({
|
||||
funding: ImmutableMap(),
|
||||
goals: ImmutableList(),
|
||||
url: '',
|
||||
});
|
||||
|
||||
const ReducerRecord = ImmutableRecord({
|
||||
instance: PatronInstanceRecord() as PatronInstance,
|
||||
accounts: ImmutableMap<string, PatronAccount>(),
|
||||
});
|
||||
|
||||
type State = ReturnType<typeof ReducerRecord>;
|
||||
|
||||
export type PatronAccount = ReturnType<typeof PatronAccountRecord>;
|
||||
export type PatronInstance = ReturnType<typeof PatronInstanceRecord>;
|
||||
|
||||
const normalizePatronAccount = (state: State, account: Record<string, any>) => {
|
||||
const normalized = PatronAccountRecord(account);
|
||||
return state.setIn(['accounts', normalized.url], normalized);
|
||||
};
|
||||
|
||||
export default function patron(state = ReducerRecord(), action: AnyAction) {
|
||||
switch(action.type) {
|
||||
case PATRON_INSTANCE_FETCH_SUCCESS:
|
||||
return state.set('instance', PatronInstanceRecord(ImmutableMap(fromJS(action.instance))));
|
||||
case PATRON_ACCOUNT_FETCH_SUCCESS:
|
||||
return normalizePatronAccount(state, action.account);
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
|
@ -26,7 +26,7 @@ const getAccountMeta = (state: RootState, id: string) => state.accounts_
|
|||
const getAccountAdminData = (state: RootState, id: string) => state.admin.users.get(id);
|
||||
const getAccountPatron = (state: RootState, id: string) => {
|
||||
const url = state.accounts.get(id)?.url;
|
||||
return state.patron.getIn(['accounts', url]);
|
||||
return url ? state.patron.accounts.get(url) : null;
|
||||
};
|
||||
|
||||
export const makeGetAccount = () => {
|
||||
|
@ -47,7 +47,7 @@ export const makeGetAccount = () => {
|
|||
map.set('pleroma', meta.get('pleroma', ImmutableMap()).merge(base.get('pleroma', ImmutableMap()))); // Lol, thanks Pleroma
|
||||
map.set('relationship', relationship);
|
||||
map.set('moved', moved || null);
|
||||
map.set('patron', patron);
|
||||
map.set('patron', patron || null);
|
||||
map.setIn(['pleroma', 'admin'], admin);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -63,7 +63,6 @@
|
|||
@import 'components/getting-started';
|
||||
@import 'components/promo-panel';
|
||||
@import 'components/still-image';
|
||||
@import 'components/badge';
|
||||
@import 'components/theme-toggle';
|
||||
@import 'components/trends';
|
||||
@import 'components/wtf-panel';
|
||||
|
|
|
@ -1,23 +0,0 @@
|
|||
.badge {
|
||||
@apply inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-primary-600 text-white;
|
||||
|
||||
&--patron {
|
||||
@apply bg-primary-600 text-white;
|
||||
}
|
||||
|
||||
&--admin {
|
||||
@apply bg-black;
|
||||
}
|
||||
|
||||
&--moderator {
|
||||
@apply bg-cyan-600 text-white;
|
||||
}
|
||||
|
||||
&--bot {
|
||||
@apply bg-gray-100 text-gray-800;
|
||||
}
|
||||
|
||||
&--opaque {
|
||||
@apply bg-white bg-opacity-75 text-gray-900;
|
||||
}
|
||||
}
|
|
@ -198,16 +198,3 @@ body.admin {
|
|||
padding: 15px;
|
||||
}
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
height: 8px;
|
||||
width: 100%;
|
||||
border-radius: 4px;
|
||||
background: var(--background-color);
|
||||
overflow: hidden;
|
||||
|
||||
&__progress {
|
||||
height: 100%;
|
||||
background: var(--brand-color);
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue