kopia lustrzana https://gitlab.com/soapbox-pub/soapbox
Merge branch 'create-ui-adm-zap-split' into 'main'
Create ui adm zap split Closes #1686 See merge request soapbox-pub/soapbox!3116environments/review-main-yi2y9f/deployments/4834
commit
0880ce33d3
|
@ -0,0 +1,151 @@
|
|||
import { useState, useEffect } from 'react';
|
||||
import { defineMessages } from 'react-intl';
|
||||
|
||||
import { type INewAccount } from 'soapbox/features/admin/manage-zap-split';
|
||||
import { useApi } from 'soapbox/hooks';
|
||||
import { baseZapAccountSchema, ZapSplitData } from 'soapbox/schemas/zap-split';
|
||||
import toast from 'soapbox/toast';
|
||||
|
||||
|
||||
const messages = defineMessages({
|
||||
zapSplitFee: { id: 'manage.zap_split.fees_error_message', defaultMessage: 'The fees cannot exceed 50% of the total zap.' },
|
||||
fetchErrorMessage: { id: 'manage.zap_split.fetch_fail_request', defaultMessage: 'Failed to fetch Zap Split data.' },
|
||||
errorMessage: { id: 'manage.zap_split.fail_request', defaultMessage: 'Failed to update fees.' },
|
||||
sucessMessage: { id: 'manage.zap_split.success_request', defaultMessage: 'Fees updated successfully.' },
|
||||
});
|
||||
|
||||
/**
|
||||
* Custom hook that manages the logic for handling Zap Split data, including fetching, updating, and removing accounts.
|
||||
* It handles the state for formatted data, weights, and messages associated with the Zap Split accounts.
|
||||
*
|
||||
* @returns An object with data, weights, message, and functions to manipulate them.
|
||||
*/
|
||||
export const useManageZapSplit = () => {
|
||||
const api = useApi();
|
||||
const [formattedData, setFormattedData] = useState<ZapSplitData[]>([]);
|
||||
const [weights, setWeights] = useState<{ [id: string]: number }>({});
|
||||
const [message, setMessage] = useState<{ [id: string]: string }>({});
|
||||
|
||||
/**
|
||||
* Fetches the Zap Split data from the API, parses it, and sets the state for formatted data, weights, and messages.
|
||||
* Displays an error toast if the request fails.
|
||||
*/
|
||||
const fetchZapSplitData = async () => {
|
||||
try {
|
||||
const { data } = await api.get<ZapSplitData[]>('/api/v1/ditto/zap_splits');
|
||||
if (data) {
|
||||
const normalizedData = data.map((dataSplit) => baseZapAccountSchema.parse(dataSplit));
|
||||
setFormattedData(normalizedData);
|
||||
|
||||
const initialWeights = normalizedData.reduce((acc, item) => {
|
||||
acc[item.account.id] = item.weight;
|
||||
return acc;
|
||||
}, {} as { [id: string]: number });
|
||||
setWeights(initialWeights);
|
||||
|
||||
const initialMessages = normalizedData.reduce((acc, item) => {
|
||||
acc[item.account.id] = item.message;
|
||||
return acc;
|
||||
}, {} as { [id: string]: string });
|
||||
setMessage(initialMessages);
|
||||
}
|
||||
} catch (error) {
|
||||
toast.error(messages.fetchErrorMessage);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchZapSplitData();
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* Updates the weight of a specific account.
|
||||
*
|
||||
* @param accountId - The ID of the account whose weight is being changed.
|
||||
* @param newWeight - The new weight value to be assigned to the account.
|
||||
*/
|
||||
const handleWeightChange = (accountId: string, newWeight: number) => {
|
||||
setWeights((prevWeights) => ({
|
||||
...prevWeights,
|
||||
[accountId]: newWeight,
|
||||
}));
|
||||
};
|
||||
|
||||
/**
|
||||
* Updates the message of a specific account.
|
||||
*
|
||||
* @param accountId - The ID of the account whose weight is being changed.
|
||||
* @param newMessage - The new message to be assigned to the account.
|
||||
*/
|
||||
const handleMessageChange = (accountId: string, newMessage: string) => {
|
||||
setMessage((prevMessage) => ({
|
||||
...prevMessage,
|
||||
[accountId]: newMessage,
|
||||
}));
|
||||
};
|
||||
|
||||
/**
|
||||
* Sends the updated Zap Split data to the API, including any new account or message changes.
|
||||
* If the total weight exceeds 50%, displays an error toast and aborts the operation.
|
||||
*
|
||||
* @param newAccount - (Optional) A new account object to be added to the Zap Split data.
|
||||
*/
|
||||
const sendNewSplit = async (newAccount?: INewAccount) => {
|
||||
try {
|
||||
const updatedZapSplits = formattedData.reduce((acc: { [id: string]: { message: string; weight: number } }, zapData) => {
|
||||
acc[zapData.account.id] = {
|
||||
message: message[zapData.account.id] || zapData.message,
|
||||
weight: weights[zapData.account.id] || zapData.weight,
|
||||
};
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
if (newAccount) {
|
||||
updatedZapSplits[newAccount.acc] = {
|
||||
message: newAccount.message,
|
||||
weight: newAccount.weight,
|
||||
};
|
||||
}
|
||||
|
||||
const totalWeight = Object.values(updatedZapSplits).reduce((acc, currentValue) => {
|
||||
return acc + currentValue.weight;
|
||||
}, 0);
|
||||
|
||||
if (totalWeight > 50) {
|
||||
toast.error(messages.zapSplitFee);
|
||||
return;
|
||||
}
|
||||
|
||||
await api.put('/api/v1/admin/ditto/zap_splits', updatedZapSplits);
|
||||
} catch (error) {
|
||||
toast.error(messages.errorMessage);
|
||||
return;
|
||||
}
|
||||
|
||||
await fetchZapSplitData();
|
||||
toast.success(messages.sucessMessage);
|
||||
};
|
||||
|
||||
/**
|
||||
* Removes an account from the Zap Split by making a DELETE request to the API, and then refetches the updated data.
|
||||
*
|
||||
* @param accountId - The ID of the account to be removed.
|
||||
*/
|
||||
const removeAccount = async (accountId: string) => {
|
||||
const isToDelete = [(formattedData.find(item => item.account.id === accountId))?.account.id];
|
||||
|
||||
await api.delete('/api/v1/admin/ditto/zap_splits/', { data: isToDelete });
|
||||
await fetchZapSplitData();
|
||||
};
|
||||
|
||||
|
||||
return {
|
||||
formattedData,
|
||||
weights,
|
||||
message,
|
||||
handleMessageChange,
|
||||
handleWeightChange,
|
||||
sendNewSplit,
|
||||
removeAccount,
|
||||
};
|
||||
};
|
|
@ -60,7 +60,7 @@ const Slider: React.FC<ISlider> = ({ value, onChange }) => {
|
|||
<div className='absolute top-1/2 h-1 w-full -translate-y-1/2 rounded-full bg-primary-200 dark:bg-primary-700' />
|
||||
<div className='absolute top-1/2 h-1 -translate-y-1/2 rounded-full bg-accent-500' style={{ width: `${value * 100}%` }} />
|
||||
<span
|
||||
className='absolute top-1/2 z-10 -ml-1.5 h-3 w-3 -translate-y-1/2 rounded-full bg-accent-500 shadow'
|
||||
className='absolute top-1/2 z-[9] -ml-1.5 h-3 w-3 -translate-y-1/2 rounded-full bg-accent-500 shadow'
|
||||
tabIndex={0}
|
||||
style={{ left: `${value * 100}%` }}
|
||||
/>
|
||||
|
|
|
@ -0,0 +1,140 @@
|
|||
import React, { useState } from 'react';
|
||||
import { FormattedMessage, defineMessages, useIntl } from 'react-intl';
|
||||
|
||||
import Account from 'soapbox/components/account';
|
||||
import List, { ListItem } from 'soapbox/components/list';
|
||||
import { Button, Column, HStack, Input, Stack } from 'soapbox/components/ui';
|
||||
|
||||
import { useManageZapSplit } from '../../api/hooks/admin/useManageZapSplit';
|
||||
import AddNewAccount from '../ui/components/new-account-zap-split';
|
||||
import { ZapSplitSlider } from '../zap/components/zap-split-account-item';
|
||||
|
||||
const closeIcon = require('@tabler/icons/outline/x.svg');
|
||||
|
||||
const messages = defineMessages({
|
||||
heading: { id: 'column.admin.zap_split', defaultMessage: 'Manage Zap Split' },
|
||||
});
|
||||
|
||||
interface INewAccount{
|
||||
acc: string;
|
||||
message: string;
|
||||
weight: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Main component that handles the logic and UI for managing accounts in Zap Split.
|
||||
* Allows the user to view and edit associated accounts, adjust weights, and add new accounts.
|
||||
*
|
||||
* @returns A component that renders the Zap Split account management interface.
|
||||
*/
|
||||
const ManageZapSplit: React.FC = () => {
|
||||
const intl = useIntl();
|
||||
const { formattedData, weights, message, handleMessageChange, handleWeightChange, sendNewSplit, removeAccount } = useManageZapSplit();
|
||||
const [hasNewAccount, setHasNewAccount] = useState(false);
|
||||
const [newWeight, setNewWeight] = useState(0.05);
|
||||
const [newAccount, setNewAccount] = useState<INewAccount>({ acc: '', message: '', weight: Number((newWeight * 100).toFixed()) });
|
||||
|
||||
/**
|
||||
* Function that handles submitting a new account to Zap Split. It resets the form and triggers
|
||||
* the submission of the account with the current data.
|
||||
*/
|
||||
const handleNewAccount = () => {
|
||||
setHasNewAccount(false);
|
||||
|
||||
sendNewSplit(newAccount);
|
||||
|
||||
setNewWeight(0.05);
|
||||
setNewAccount(({ acc: '', message: '', weight: Number((newWeight * 100).toFixed()) }));
|
||||
};
|
||||
|
||||
/**
|
||||
* Updates the weight of the new account and adjusts the `newAccount` state with the new weight value.
|
||||
*
|
||||
* @param newWeight - The new weight assigned to the account.
|
||||
*/
|
||||
const handleChange = (newWeight: number) => {
|
||||
setNewWeight(newWeight);
|
||||
setNewAccount((previousValue) => ({
|
||||
...previousValue,
|
||||
weight: Number((newWeight * 100).toFixed()) }));
|
||||
};
|
||||
|
||||
/**
|
||||
* Formats the weight value into an integer representing the percentage.
|
||||
*
|
||||
* @param weight - The weight as a decimal number (e.g., 0.05).
|
||||
* @returns The formatted weight as an integer (e.g., 5 for 5%).
|
||||
*/
|
||||
const formattedWeight = (weight: number) =>{
|
||||
return Number((weight * 100).toFixed());
|
||||
};
|
||||
|
||||
return (
|
||||
<Column label={intl.formatMessage(messages.heading)}>
|
||||
<Stack space={4} >
|
||||
<List>
|
||||
{formattedData.map((data) => (
|
||||
<ListItem key={data.account.id} label={''}>
|
||||
<div className='relative flex w-full flex-col items-start justify-center gap-4 pt-2 md:flex-row md:items-center md:justify-between md:pt-0'>
|
||||
<div className='flex min-w-60 items-center justify-center md:justify-start'>
|
||||
<Account account={data.account} showProfileHoverCard={false} />
|
||||
</div>
|
||||
|
||||
<div className='flex w-[96%] flex-col justify-center md:w-full'>
|
||||
<FormattedMessage id='manage.zap_split.new_account_message' defaultMessage='Message:' />
|
||||
<Input
|
||||
className='md:-mt-1'
|
||||
value={message[data.account.id] || ''}
|
||||
onChange={(e) =>
|
||||
handleMessageChange(data.account.id, e.target.value)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<HStack space={2} className='w-full md:justify-end'>
|
||||
<ZapSplitSlider
|
||||
width='w-[96%] md:w-40'
|
||||
initialWeight={weights[data.account.id] || 0}
|
||||
onWeightChange={(weight) => handleWeightChange(data.account.id, weight)}
|
||||
/>
|
||||
<Button type='button' className='absolute right-0 top-0 md:relative' size='xs' icon={closeIcon} theme='transparent' onClick={() => removeAccount(data.account.id)} />
|
||||
</HStack>
|
||||
</div>
|
||||
</ListItem>
|
||||
))}
|
||||
|
||||
{hasNewAccount && (
|
||||
<AddNewAccount
|
||||
newAccount={newAccount}
|
||||
newWeight={newWeight}
|
||||
setNewAccount={setNewAccount}
|
||||
handleChange={handleChange}
|
||||
formattedWeight={formattedWeight}
|
||||
closeNewAccount={() => {
|
||||
setHasNewAccount(false);
|
||||
setNewWeight(0.05);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
</List>
|
||||
{hasNewAccount && <Button theme='secondary' type='button' onClick={handleNewAccount}>
|
||||
<FormattedMessage id='manage.zap_split.add_new_account' defaultMessage='Add New Account' />
|
||||
</Button>}
|
||||
{!hasNewAccount && <Button theme='secondary' type='button' onClick={() => setHasNewAccount(true)}>
|
||||
<FormattedMessage id='manage.zap_split.add' defaultMessage='Add' />
|
||||
</Button>}
|
||||
<HStack space={2} justifyContent='end'>
|
||||
<Button to='/admin' theme='tertiary'>
|
||||
<FormattedMessage id='common.cancel' defaultMessage='Cancel' />
|
||||
</Button>
|
||||
<Button type='button' theme='primary' onClick={() => sendNewSplit()}>
|
||||
<FormattedMessage id='manage.zap_split.save' defaultMessage='Save' />
|
||||
</Button>
|
||||
</HStack>
|
||||
</Stack>
|
||||
</Column>
|
||||
);
|
||||
};
|
||||
|
||||
export default ManageZapSplit;
|
||||
export type { INewAccount };
|
|
@ -93,6 +93,11 @@ const Dashboard: React.FC = () => {
|
|||
label={<FormattedMessage id='column.admin.moderation_log' defaultMessage='Moderation Log' />}
|
||||
/>
|
||||
|
||||
<ListItem
|
||||
to='/soapbox/admin/zap-split'
|
||||
label={<FormattedMessage id='column.admin.zap_split' defaultMessage='Manage Zap Split' />}
|
||||
/>
|
||||
|
||||
{features.adminAnnouncements && (
|
||||
<ListItem
|
||||
to='/soapbox/admin/announcements'
|
||||
|
|
|
@ -0,0 +1,168 @@
|
|||
import clsx from 'clsx';
|
||||
import debounce from 'lodash/debounce';
|
||||
import React, { useCallback, useEffect } from 'react';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
|
||||
import {
|
||||
changeSearch,
|
||||
clearSearchResults,
|
||||
setSearchAccount,
|
||||
showSearch,
|
||||
submitSearch,
|
||||
} from 'soapbox/actions/search';
|
||||
import AutosuggestAccountInput from 'soapbox/components/autosuggest-account-input';
|
||||
import { Input } from 'soapbox/components/ui';
|
||||
import SvgIcon from 'soapbox/components/ui/icon/svg-icon';
|
||||
import { useAppDispatch, useAppSelector } from 'soapbox/hooks';
|
||||
import { Account } from 'soapbox/schemas';
|
||||
import { selectAccount } from 'soapbox/selectors';
|
||||
import { RootState } from 'soapbox/store';
|
||||
|
||||
const messages = defineMessages({
|
||||
placeholder: { id: 'search.placeholder', defaultMessage: 'Search' },
|
||||
action: { id: 'search.action', defaultMessage: 'Search for “{query}”' },
|
||||
});
|
||||
|
||||
interface ISearchZapSplit {
|
||||
autoFocus?: boolean;
|
||||
autoSubmit?: boolean;
|
||||
autosuggest?: boolean;
|
||||
openInRoute?: boolean;
|
||||
onChange: (account: Account | null) => void;
|
||||
}
|
||||
|
||||
const SearchZapSplit = (props: ISearchZapSplit) => {
|
||||
const {
|
||||
autoFocus = false,
|
||||
autoSubmit = false,
|
||||
autosuggest = false,
|
||||
openInRoute = false,
|
||||
} = props;
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
const history = useHistory();
|
||||
const intl = useIntl();
|
||||
|
||||
const value = useAppSelector((state) => state.search.value);
|
||||
const submitted = useAppSelector((state) => state.search.submitted);
|
||||
|
||||
const debouncedSubmit = useCallback(debounce(() => {
|
||||
dispatch(submitSearch());
|
||||
}, 900), []);
|
||||
|
||||
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const { value } = event.target;
|
||||
|
||||
dispatch(changeSearch(value));
|
||||
|
||||
if (autoSubmit) {
|
||||
debouncedSubmit();
|
||||
}
|
||||
};
|
||||
|
||||
const handleClear = (event: React.MouseEvent<HTMLDivElement>) => {
|
||||
event.preventDefault();
|
||||
|
||||
if (value.length > 0 || submitted) {
|
||||
dispatch(clearSearchResults());
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmit = () => {
|
||||
if (openInRoute) {
|
||||
dispatch(setSearchAccount(null));
|
||||
dispatch(submitSearch());
|
||||
|
||||
history.push('/search');
|
||||
} else {
|
||||
dispatch(submitSearch());
|
||||
}
|
||||
};
|
||||
|
||||
const handleKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
if (event.key === 'Enter') {
|
||||
event.preventDefault();
|
||||
|
||||
handleSubmit();
|
||||
} else if (event.key === 'Escape') {
|
||||
document.querySelector('.ui')?.parentElement?.focus();
|
||||
}
|
||||
};
|
||||
|
||||
const handleFocus = () => {
|
||||
dispatch(showSearch());
|
||||
};
|
||||
|
||||
const getAccount = (accountId: string) => (dispatch: any, getState: () => RootState) => {
|
||||
const account = selectAccount(getState(), accountId);
|
||||
console.log(account);
|
||||
|
||||
props.onChange(account!);
|
||||
};
|
||||
|
||||
const handleSelected = (accountId: string) => {
|
||||
dispatch(getAccount(accountId));
|
||||
};
|
||||
|
||||
const hasValue = value.length > 0 || submitted;
|
||||
const componentProps: any = {
|
||||
type: 'text',
|
||||
id: 'search',
|
||||
placeholder: intl.formatMessage(messages.placeholder),
|
||||
value,
|
||||
onChange: handleChange,
|
||||
onKeyDown: handleKeyDown,
|
||||
onFocus: handleFocus,
|
||||
autoFocus: autoFocus,
|
||||
theme: 'normal',
|
||||
className: 'pr-10 rtl:pl-10 rtl:pr-3',
|
||||
};
|
||||
|
||||
if (autosuggest) {
|
||||
componentProps.onSelected = handleSelected;
|
||||
componentProps.autoSelect = false;
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
const newPath = history.location.pathname;
|
||||
const shouldPersistSearch = !!newPath.match(/@.+\/posts\/[a-zA-Z0-9]+/g)
|
||||
|| !!newPath.match(/\/tags\/.+/g);
|
||||
|
||||
if (!shouldPersistSearch) {
|
||||
dispatch(changeSearch(''));
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className='w-full'>
|
||||
<label htmlFor='search' className='sr-only'>{intl.formatMessage(messages.placeholder)}</label>
|
||||
|
||||
<div className='relative'>
|
||||
{autosuggest ? (
|
||||
<AutosuggestAccountInput {...componentProps} />
|
||||
) : (
|
||||
<Input theme='normal' {...componentProps} />
|
||||
)}
|
||||
|
||||
<div
|
||||
role='button'
|
||||
tabIndex={0}
|
||||
className='absolute inset-y-0 right-0 flex cursor-pointer items-center px-3 rtl:left-0 rtl:right-auto'
|
||||
onClick={handleClear}
|
||||
>
|
||||
|
||||
<SvgIcon
|
||||
src={require('@tabler/icons/outline/x.svg')}
|
||||
className={clsx('h-4 w-4 text-gray-600', { hidden: !hasValue })}
|
||||
aria-label={intl.formatMessage(messages.placeholder)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SearchZapSplit;
|
|
@ -34,7 +34,6 @@ import {
|
|||
MuteModal,
|
||||
NostrLoginModal,
|
||||
NostrSignupModal,
|
||||
OnboardingFlowModal,
|
||||
ReactionsModal,
|
||||
ReblogsModal,
|
||||
ReplyMentionsModal,
|
||||
|
@ -86,7 +85,6 @@ const MODAL_COMPONENTS: Record<string, React.LazyExoticComponent<any>> = {
|
|||
'MUTE': MuteModal,
|
||||
'NOSTR_LOGIN': NostrLoginModal,
|
||||
'NOSTR_SIGNUP': NostrSignupModal,
|
||||
'ONBOARDING_FLOW': OnboardingFlowModal,
|
||||
'REACTIONS': ReactionsModal,
|
||||
'REBLOGS': ReblogsModal,
|
||||
'REPLY_MENTIONS': ReplyMentionsModal,
|
||||
|
|
|
@ -0,0 +1,96 @@
|
|||
import React, { useEffect, useState } from 'react';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
import Account from 'soapbox/components/account';
|
||||
import { ListItem } from 'soapbox/components/list';
|
||||
import { Button, HStack, Input, Slider } from 'soapbox/components/ui';
|
||||
import SearchZapSplit from 'soapbox/features/compose/components/search-zap-split';
|
||||
import { type Account as AccountEntity } from 'soapbox/schemas';
|
||||
|
||||
const closeIcon = require('@tabler/icons/outline/x.svg');
|
||||
|
||||
interface INewAccount {
|
||||
acc: string;
|
||||
message: string;
|
||||
weight: number;
|
||||
}
|
||||
|
||||
interface AddNewAccountProps {
|
||||
newAccount: INewAccount;
|
||||
newWeight: number;
|
||||
setNewAccount: React.Dispatch<React.SetStateAction<INewAccount>>;
|
||||
handleChange: (newWeight: number) => void;
|
||||
formattedWeight: (weight: number) => number;
|
||||
closeNewAccount: () => void;
|
||||
}
|
||||
|
||||
const AddNewAccount: React.FC<AddNewAccountProps> = ({
|
||||
newAccount,
|
||||
newWeight,
|
||||
setNewAccount,
|
||||
handleChange,
|
||||
formattedWeight,
|
||||
closeNewAccount,
|
||||
}) => {
|
||||
|
||||
const [accountSelected, setAccountSelected] = useState<AccountEntity | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (accountSelected) {
|
||||
setNewAccount((previousValue) => ({
|
||||
...previousValue,
|
||||
acc: accountSelected?.id || '',
|
||||
}));
|
||||
}
|
||||
}, [accountSelected, setNewAccount]);
|
||||
|
||||
return (
|
||||
<ListItem label={<div className='' />}>
|
||||
<div className='relative flex w-full flex-col items-center justify-between gap-4 pt-2 md:flex-row md:pt-0'>
|
||||
|
||||
{accountSelected ?
|
||||
<div className='flex w-full items-center md:w-60 '>
|
||||
<Account account={accountSelected} />
|
||||
</div>
|
||||
:
|
||||
<div className='flex w-[96%] flex-col items-start justify-center md:max-w-60'>
|
||||
<FormattedMessage id='manage.zap_split.new_account' defaultMessage='Account:' />
|
||||
<SearchZapSplit autosuggest onChange={setAccountSelected} />
|
||||
</div>
|
||||
}
|
||||
|
||||
<div className='flex w-[96%] flex-col justify-center md:w-full'>
|
||||
<FormattedMessage id='manage.zap_split.new_account_message' defaultMessage='Message:' />
|
||||
<Input
|
||||
className='md:-mt-1'
|
||||
value={newAccount.message}
|
||||
onChange={(e) =>
|
||||
setNewAccount((previousValue) => ({
|
||||
...previousValue,
|
||||
message: e.target.value,
|
||||
}))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<HStack space={2} className='w-full md:justify-end'>
|
||||
<div className='flex w-[96%] flex-col md:w-40'>
|
||||
{formattedWeight(newWeight)}%
|
||||
<Slider value={newWeight} onChange={(e) => handleChange(e)} />
|
||||
</div>
|
||||
|
||||
<Button
|
||||
type='button'
|
||||
size='xs'
|
||||
icon={closeIcon}
|
||||
className='absolute right-0 top-0 md:relative'
|
||||
theme='transparent'
|
||||
onClick={closeNewAccount}
|
||||
/>
|
||||
</HStack>
|
||||
</div>
|
||||
</ListItem>
|
||||
);
|
||||
};
|
||||
|
||||
export default AddNewAccount;
|
|
@ -29,6 +29,7 @@ import GroupsPendingPage from 'soapbox/pages/groups-pending-page';
|
|||
import HomePage from 'soapbox/pages/home-page';
|
||||
import LandingPage from 'soapbox/pages/landing-page';
|
||||
import ManageGroupsPage from 'soapbox/pages/manage-groups-page';
|
||||
import ManageZapSplitPage from 'soapbox/pages/manage-zap-split-page';
|
||||
import ProfilePage from 'soapbox/pages/profile-page';
|
||||
import RemoteInstancePage from 'soapbox/pages/remote-instance-page';
|
||||
import SearchPage from 'soapbox/pages/search-page';
|
||||
|
@ -140,6 +141,7 @@ import {
|
|||
NostrRelays,
|
||||
Bech32Redirect,
|
||||
Relays,
|
||||
ManageZapSplit,
|
||||
Rules,
|
||||
AdminNostrRelays,
|
||||
} from './util/async-components';
|
||||
|
@ -331,6 +333,7 @@ const SwitchingColumnsArea: React.FC<ISwitchingColumnsArea> = ({ children }) =>
|
|||
<WrappedRoute path='/soapbox/admin/approval' staffOnly page={AdminPage} component={Dashboard} content={children} exact />
|
||||
<WrappedRoute path='/soapbox/admin/reports' staffOnly page={AdminPage} component={Dashboard} content={children} exact />
|
||||
<WrappedRoute path='/soapbox/admin/log' staffOnly page={AdminPage} component={ModerationLog} content={children} exact />
|
||||
<WrappedRoute path='/soapbox/admin/zap-split' staffOnly page={ManageZapSplitPage} component={ManageZapSplit} content={children} exact />
|
||||
<WrappedRoute path='/soapbox/admin/users' staffOnly page={AdminPage} component={UserIndex} content={children} exact />
|
||||
<WrappedRoute path='/soapbox/admin/theme' staffOnly page={AdminPage} component={ThemeEditor} content={children} exact />
|
||||
<WrappedRoute path='/soapbox/admin/relays' staffOnly page={AdminPage} component={Relays} content={children} exact />
|
||||
|
|
|
@ -163,7 +163,6 @@ export const AccountNotePanel = lazy(() => import('soapbox/features/ui/component
|
|||
export const ComposeEditor = lazy(() => import('soapbox/features/compose/editor'));
|
||||
export const NostrSignupModal = lazy(() => import('soapbox/features/ui/components/modals/nostr-signup-modal/nostr-signup-modal'));
|
||||
export const NostrLoginModal = lazy(() => import('soapbox/features/ui/components/modals/nostr-login-modal/nostr-login-modal'));
|
||||
export const OnboardingFlowModal = lazy(() => import ('soapbox/features/ui/components/modals/onboarding-flow-modal/onboarding-flow-modal'));
|
||||
export const BookmarkFolders = lazy(() => import('soapbox/features/bookmark-folders'));
|
||||
export const EditBookmarkFolderModal = lazy(() => import('soapbox/features/ui/components/modals/edit-bookmark-folder-modal'));
|
||||
export const SelectBookmarkFolderModal = lazy(() => import('soapbox/features/ui/components/modals/select-bookmark-folder-modal'));
|
||||
|
@ -172,6 +171,7 @@ export const Domains = lazy(() => import('soapbox/features/admin/domains'));
|
|||
export const EditDomainModal = lazy(() => import('soapbox/features/ui/components/modals/edit-domain-modal'));
|
||||
export const NostrRelays = lazy(() => import('soapbox/features/nostr-relays'));
|
||||
export const Bech32Redirect = lazy(() => import('soapbox/features/nostr/Bech32Redirect'));
|
||||
export const ManageZapSplit = lazy(() => import('soapbox/features/admin/manage-zap-split'));
|
||||
export const Relays = lazy(() => import('soapbox/features/admin/relays'));
|
||||
export const Rules = lazy(() => import('soapbox/features/admin/rules'));
|
||||
export const EditRuleModal = lazy(() => import('soapbox/features/ui/components/modals/edit-rule-modal'));
|
||||
|
|
|
@ -0,0 +1,35 @@
|
|||
import clsx from 'clsx';
|
||||
import React, { useState } from 'react';
|
||||
|
||||
import { Slider } from 'soapbox/components/ui';
|
||||
|
||||
const formattedWeight = (weight: number) =>{
|
||||
return Number((weight * 100).toFixed());
|
||||
};
|
||||
|
||||
interface IZapSplitSlider {
|
||||
width?: string;
|
||||
initialWeight: number;
|
||||
onWeightChange: (newWeight: number) => void;
|
||||
}
|
||||
|
||||
export const ZapSplitSlider: React.FC<IZapSplitSlider> = ({ initialWeight, onWeightChange, width = 'w-40' }) => {
|
||||
const [value, setValue] = useState(initialWeight / 100);
|
||||
|
||||
const handleChange = (newValue: number) => {
|
||||
setValue(newValue);
|
||||
onWeightChange(Number((newValue * 100).toFixed()));
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={clsx('flex flex-col', width)}>
|
||||
{formattedWeight(value)}%
|
||||
<Slider
|
||||
value={value}
|
||||
onChange={(v) => {
|
||||
handleChange(v);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
|
@ -347,6 +347,7 @@
|
|||
"column.admin.reports.menu.moderation_log": "Moderation Log",
|
||||
"column.admin.rules": "Instance rules",
|
||||
"column.admin.users": "Users",
|
||||
"column.admin.zap_split": "Manage Zap Split",
|
||||
"column.aliases": "Account aliases",
|
||||
"column.aliases.create_error": "Error creating alias",
|
||||
"column.aliases.delete": "Delete",
|
||||
|
@ -1032,6 +1033,15 @@
|
|||
"login_external.errors.instance_fail": "The instance returned an error.",
|
||||
"login_external.errors.network_fail": "Connection failed. Is a browser extension blocking it?",
|
||||
"login_form.header": "Sign In",
|
||||
"manage.zap_split.add": "Add",
|
||||
"manage.zap_split.add_new_account": "Add New Account",
|
||||
"manage.zap_split.fail_request": "Failed to update fees.",
|
||||
"manage.zap_split.fees_error_message": "The fees cannot exceed 50% of the total zap.",
|
||||
"manage.zap_split.fetch_fail_request": "Failed to fetch Zap Split data.",
|
||||
"manage.zap_split.new_account": "Account:",
|
||||
"manage.zap_split.new_account_message": "Message:",
|
||||
"manage.zap_split.save": "Save",
|
||||
"manage.zap_split.success_request": "Fees updated successfully.",
|
||||
"manage_group.blocked_members": "Banned Members",
|
||||
"manage_group.confirmation.copy": "Copy link",
|
||||
"manage_group.confirmation.info_1": "As the owner of this group, you can assign staff, delete posts and much more.",
|
||||
|
|
|
@ -0,0 +1,16 @@
|
|||
import React from 'react';
|
||||
|
||||
interface IManageZapSplitPage {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
/** Custom layout for Manage Zap Split on desktop. */
|
||||
const ManageZapSplitPage: React.FC<IManageZapSplitPage> = ({ children }) => {
|
||||
return (
|
||||
<div className='black:border-gray-800 md:col-span-12 lg:col-span-9 lg:black:border-l'>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ManageZapSplitPage;
|
Ładowanie…
Reference in New Issue