Merge branch 'create-ui-adm-zap-split' into 'main'

Create ui adm zap split

Closes #1686

See merge request soapbox-pub/soapbox!3116
environments/review-main-yi2y9f/deployments/4834
Alex Gleason 2024-09-24 20:23:56 +00:00
commit 0880ce33d3
12 zmienionych plików z 626 dodań i 4 usunięć

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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