starts donation modal

This commit is contained in:
ChaosExAnima 2025-09-11 16:35:03 +02:00
parent 41ab10f88c
commit 89e4fb2f0f
No known key found for this signature in database
GPG Key ID: 8F2B333100FB6117
7 changed files with 221 additions and 4 deletions

View File

@ -24,13 +24,13 @@ interface PropsWithText extends BaseProps {
children?: undefined;
}
type Props = PropsWithText | PropsChildren;
export type ButtonProps = PropsWithText | PropsChildren;
/**
* Primary UI component for user interaction that doesn't result in navigation.
*/
export const Button: React.FC<Props> = ({
export const Button: React.FC<ButtonProps> = ({
type = 'button',
onClick,
disabled,

View File

@ -21,7 +21,7 @@ interface DropdownProps {
items: SelectItem[];
onChange: (value: string) => void;
current: string;
labelId: string;
labelId?: string;
descriptionId?: string;
emptyText?: MessageDescriptor;
classPrefix: string;
@ -79,7 +79,7 @@ export const Dropdown: FC<
type='button'
{...buttonProps}
id={buttonId}
aria-labelledby={`${labelId} ${buttonId}`}
aria-labelledby={classNames(labelId, buttonId)}
aria-describedby={descriptionId}
aria-expanded={open}
aria-controls={listboxId}

View File

@ -0,0 +1,163 @@
import type { FC, SyntheticEvent } from 'react';
import { forwardRef, useCallback, useState } from 'react';
import { defineMessages, useIntl } from 'react-intl';
import classNames from 'classnames';
import type { ButtonProps } from '@/mastodon/components/button';
import { Button } from '@/mastodon/components/button';
import { Dropdown } from '@/mastodon/components/dropdown';
import type { SelectItem } from '@/mastodon/components/dropdown_selector';
import { IconButton } from '@/mastodon/components/icon_button';
import CloseIcon from '@/material-icons/400-24px/close.svg?react';
import type { BaseConfirmationModalProps } from './confirmation_modals/confirmation_modal';
import './donate_model.scss';
type DonateModalProps = BaseConfirmationModalProps;
type Frequency = 'one_time' | 'monthly';
const messages = defineMessages({
close: { id: 'lightbox.close', defaultMessage: 'Close' },
});
const currencyOptions = [
{
value: 'usd',
text: 'USD',
},
{
value: 'eur',
text: 'EUR',
},
] as const satisfies SelectItem[];
type Currency = (typeof currencyOptions)[number]['value'];
function isCurrency(value: string): value is Currency {
return currencyOptions.some((option) => option.value === value);
}
const amountOptions = [
{ value: '2000', text: '€20' },
{ value: '1000', text: '€10' },
{ value: '500', text: '€5' },
{ value: '300', text: '€3' },
] as const satisfies SelectItem[];
// eslint-disable-next-line @typescript-eslint/no-unused-vars -- React throws a warning if not set.
const DonateModal: FC<DonateModalProps> = forwardRef(({ onClose }, ref) => {
const intl = useIntl();
const [frequency, setFrequency] = useState<Frequency>('one_time');
const handleFrequencyToggle = useCallback((value: Frequency) => {
return () => {
setFrequency(value);
};
}, []);
const [currency, setCurrency] = useState<Currency>('usd');
const handleCurrencyChange = useCallback((value: string) => {
if (isCurrency(value)) {
setCurrency(value);
}
}, []);
const [amount, setAmount] = useState(() =>
Number.parseInt(amountOptions[0].value),
);
const handleAmountChange = useCallback((event: SyntheticEvent) => {
if (
event.target instanceof HTMLButtonElement ||
event.target instanceof HTMLInputElement
) {
setAmount(Number.parseInt(event.target.value));
}
}, []);
return (
<div className='modal-root__modal dialog-modal donate_modal'>
<div className='dialog-modal__header'>
<IconButton
className='dialog-modal__header__close'
title={intl.formatMessage(messages.close)}
icon='times'
iconComponent={CloseIcon}
onClick={onClose}
/>
<span className='dialog-modal__header__title'>
By supporting Mastodon, you help sustain a global network that values
people over profit. Will you join us today?
</span>
</div>
<div className='dialog-modal__content'>
<div className='dialog-modal__content__form'>
<div className='row'>
<ToggleButton
active={frequency === 'one_time'}
onClick={handleFrequencyToggle('one_time')}
>
One Time
</ToggleButton>
<ToggleButton
active={frequency === 'monthly'}
onClick={handleFrequencyToggle('monthly')}
>
Monthly
</ToggleButton>
</div>
<div className='row row--select'>
<Dropdown
items={currencyOptions}
current={currency}
classPrefix='donate_modal'
onChange={handleCurrencyChange}
/>
<input
type='number'
min='1'
step='1'
value={amount}
onChange={handleAmountChange}
/>
</div>
<div className='row'>
{amountOptions.map((option) => (
<ToggleButton
key={option.value}
onClick={handleAmountChange}
active={amount === Number.parseInt(option.value)}
value={option.value}
text={option.text}
/>
))}
</div>
<Button className='submit' block>
Continue to payment
</Button>
</div>
</div>
</div>
);
});
DonateModal.displayName = 'DonateModal';
const ToggleButton: FC<ButtonProps & { active: boolean }> = ({
active,
...props
}) => {
return (
<Button
block
{...props}
className={classNames('toggle', props.className, { active })}
/>
);
};
// eslint-disable-next-line import/no-default-export -- modal_root expects a default export.
export default DonateModal;

View File

@ -0,0 +1,48 @@
.donate_modal {
.dialog-modal__header {
gap: 1rem;
}
.row {
display: flex;
gap: 1rem;
&--select {
gap: 0;
width: 100%;
button {
background-color: var(--button-color);
border: none;
border-top-left-radius: 4px;
border-bottom-left-radius: 4px;
color: white;
display: flex;
align-items: center;
padding: 0.5rem;
}
input {
flex-grow: 1;
background: none;
padding: 0.5rem;
color: white;
border: 1px solid var(--button-color);
border-top-right-radius: 4px;
border-bottom-right-radius: 4px;
}
}
}
.button.toggle:not(.active) {
background-color: inherit;
border: 2px solid var(--button-color);
color: var(--button-color);
&:hover {
border-color: var(--button-hover-color);
background-color: var(--button-hover-color);
color: white;
}
}
}

View File

@ -19,6 +19,7 @@ import {
ClosedRegistrationsModal,
IgnoreNotificationsModal,
AnnualReportModal,
DonateModal,
} from 'mastodon/features/ui/util/async-components';
import BundleContainer from '../containers/bundle_container';
@ -80,6 +81,7 @@ export const MODAL_COMPONENTS = {
'IGNORE_NOTIFICATIONS': IgnoreNotificationsModal,
'ANNUAL_REPORT': AnnualReportModal,
'COMPOSE_PRIVACY': () => Promise.resolve({ default: VisibilityModal }),
'DONATE': DonateModal,
};
export default class ModalRoot extends PureComponent {

View File

@ -230,6 +230,8 @@ export function AnnualReportModal () {
return import('../components/annual_report_modal');
}
export const DonateModal = () => import('../components/donate_modal');
export function ListEdit () {
return import('../../lists/new');
}

View File

@ -37,6 +37,8 @@
--input-placeholder-color: #{$dark-text-color};
--input-background-color: var(--surface-variant-background-color);
--on-input-color: #{$secondary-text-color};
--button-color: #{$ui-button-background-color};
--button-hover-color: #{$ui-button-focus-background-color};
}
body {