Restore Patron features, context to TSX

revert-5af0e40a
Alex Gleason 2022-04-24 17:01:57 -05:00
rodzic 57e5d81e33
commit 3f9cc3cd04
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: 7211D1F99744FBB7
14 zmienionych plików z 184 dodań i 154 usunięć

Wyświetl plik

@ -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"
}

Wyświetl plik

@ -0,0 +1,4 @@
{
"is_patron": true,
"url": "https://gleasonator.com/users/dave"
}

Wyświetl plik

@ -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;

Wyświetl plik

@ -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>
);

Wyświetl plik

@ -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'>&mdash; 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));

Wyświetl plik

@ -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'>&mdash; 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;

Wyświetl plik

@ -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,

Wyświetl plik

@ -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} />}

Wyświetl plik

@ -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;
}
}

Wyświetl plik

@ -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;
}
}

Wyświetl plik

@ -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);
});
});

Wyświetl plik

@ -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';

Wyświetl plik

@ -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;
}
}

Wyświetl plik

@ -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);
}
}