kopia lustrzana https://gitlab.com/soapbox-pub/soapbox
				
				
				
			Restore Patron features, context to TSX
							rodzic
							
								
									57e5d81e33
								
							
						
					
					
						commit
						3f9cc3cd04
					
				|  | @ -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" | ||||
| } | ||||
|  | @ -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)); | ||||
|  | @ -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; | ||||
|   } | ||||
| } | ||||
|  | @ -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); | ||||
|   } | ||||
| } | ||||
|  |  | |||
		Ładowanie…
	
		Reference in New Issue
	
	 Alex Gleason
						Alex Gleason