@mui/material#Avatar TypeScript Examples
The following examples show how to use
@mui/material#Avatar.
You can vote up the ones you like or vote down the ones you don't like,
and go to the original project or source file by following the links above each example. You may check out the related API usage on the sidebar.
Example #1
Source File: accountCard.tsx From Search-Next with GNU General Public License v3.0 | 6 votes |
AccountCard: React.FC<AccoundCardProps> = ({ account }) => {
return (
<div className="flex py-4 pl-0 items-center">
<div className="mr-4">
<Avatar sx={{ width: 64, height: 64 }}>
{account.username?.split('')[0].toLocaleUpperCase()}
</Avatar>
</div>
<div>
<div className="text-lg font-bold leading-7 mb-0">
{account.username}
<Chip
className="ml-2"
size="small"
color="info"
label={account.type === 'local' ? '本地账户' : '注册账户'}
></Chip>
</div>
<div className="text-sm mt-1 mb-0">
创建时间: {dayjs(account.createdTime).format('YYYY-MM-DD HH:mm')}
</div>
</div>
</div>
);
}
Example #2
Source File: MemberCard.tsx From frontend with MIT License | 6 votes |
export default function MemberCard({ info }: MemberCardProps) {
const { t } = useTranslation()
return (
<Card sx={{ backgroundColor: theme.palette.secondary.light }}>
<Box
m="auto"
width={200}
height={200}
display="flex"
alignItems="center"
justifyContent="center">
<Avatar src={info.img} sx={{ width: 150, height: 150 }} />
</Box>
<CardContent sx={{ paddingX: theme.spacing(1) }}>
<Typography gutterBottom variant="h6" component="div" align="center">
{info.title}
</Typography>
<Typography variant="subtitle1" align="center">
{info.content}
</Typography>
</CardContent>
<CardActions style={{ justifyContent: 'center' }}>
<Button
size="small"
variant="text"
style={{ color: 'black' }}
href={staticUrls.blog + info.blogLink}>
{t('common:cta.read-more')}
</Button>
<DoubleArrowIcon fontSize="inherit" />
</CardActions>
</Card>
)
}
Example #3
Source File: FileList.tsx From frontend with MIT License | 6 votes |
function FileList({ files, onDelete }: Props) {
return (
<List dense>
{files.map((file, key) => (
<ListItem
key={key}
secondaryAction={
<IconButton edge="end" aria-label="delete" onClick={() => onDelete && onDelete(file)}>
<Delete />
</IconButton>
}>
<ListItemAvatar>
<Avatar>
<UploadFile />
</Avatar>
</ListItemAvatar>
<ListItemText primary={file.type} />
<ListItemText primary={file.name} />
</ListItem>
))}
</List>
)
}
Example #4
Source File: DetailCreate.tsx From airmessage-web with Apache License 2.0 | 6 votes |
function DirectSendButton(props: {address: string, onClick: () => void}) {
const theme = useTheme();
return (
<ButtonBase
onClick={props.onClick}
sx={{
width: "100%",
padding: "8px 0",
transition: theme.transitions.create(["background-color", "box-shadow", "border"], {
duration: theme.transitions.duration.short,
}),
borderRadius: theme.shape.borderRadius,
display: "flex",
flexDirection: "row",
justifyContent: "flex-start",
"&:hover": {
backgroundColor: alpha(theme.palette.text.primary, theme.palette.action.hoverOpacity),
}
}}>
<Avatar sx={{
backgroundColor: theme.palette.primary.main,
marginRight: "16px !important"
}} />
<Typography>Send to <b>{props.address}</b></Typography>
</ButtonBase>
);
}
Example #5
Source File: Dashboard.tsx From NekoMaid with MIT License | 6 votes |
TopCard: React.FC<{ title: string, content: React.ReactNode, icon: React.ReactNode, color: string }> = ({ title, content, icon, children, color }) =>
<Card sx={{ height: '100%' }}>
<CardContent>
<Grid container spacing={3} sx={{ justifyContent: 'space-between', flexWrap: 'nowrap' }}>
<Grid item sx={{ overflow: 'hidden' }}>
<Typography color='textSecondary' gutterBottom variant='h6'>{title}</Typography>
<Typography color='textPrimary' variant='h4' noWrap sx={{ textOverflow: 'ellipsis' }}>{content}</Typography>
</Grid>
<Grid item sx={{ paddingLeft: '0 !important' }}>
<Avatar sx={{ backgroundColor: color, height: 50, width: 50 }}>{icon}</Avatar>
</Grid>
</Grid>
{children}
</CardContent>
</Card>
Example #6
Source File: App.tsx From wrap.scrt.network with MIT License | 6 votes |
ReactDOM.render( <BreakpointProvider> <React.StrictMode> <div style={{ minHeight: `calc(100vh - ${footerHeight})` }}> <App /> </div> <a href="https://SCRT.network" target="_blank" style={{ height: footerHeight, backgroundColor: "#e7e7e7", display: "flex", placeContent: "center", placeItems: "center", position: "relative", left: 0, bottom: 0, gap: "0.3em", textDecoration: "none", }} > <Avatar src="/scrt.svg" sx={{ width: "1em", height: "1em" }} /> <span style={{ color: "black" }}>Powered by Secret Network</span> </a> </React.StrictMode> </BreakpointProvider>, document.getElementById("root") );
Example #7
Source File: FileList.tsx From frontend with MIT License | 5 votes |
function FileList({ files, onDelete, onSetFileRole, filesRole = [] }: Props) {
const setFileRole = (file: File) => {
return (event: SelectChangeEvent<CampaignFileRole>) => {
if (Object.values(CampaignFileRole).includes(event.target.value as CampaignFileRole)) {
onSetFileRole(file, event.target.value as CampaignFileRole)
}
}
}
return (
<List dense>
{files.map((file, key) => (
<ListItem
key={key}
secondaryAction={
<IconButton edge="end" aria-label="delete" onClick={() => onDelete && onDelete(file)}>
<Delete />
</IconButton>
}>
<ListItemAvatar>
<Avatar>
<UploadFile />
</Avatar>
</ListItemAvatar>
<ListItemText primary={file.type} />
<ListItemText primary={file.name} />
<FormControl>
<InputLabel id="choose-type-label">{'Избери роля'}</InputLabel>
<Select<CampaignFileRole>
id="choose-type"
label="Избери роля"
labelId="choose-type-label"
value={
filesRole.find((f) => f.file === file.name)?.role ?? CampaignFileRole.background
}
onChange={setFileRole(file)}>
{Object.values(CampaignFileRole).map((role) => (
<MenuItem key={role} value={role}>
{role}
</MenuItem>
))}
</Select>
</FormControl>
</ListItem>
))}
</List>
)
}
Example #8
Source File: IrregularityFile.tsx From frontend with MIT License | 5 votes |
export default function IrregularityFile({ file, irregularityId }: Props) {
const { t } = useTranslation('irregularity')
const queryClient = useQueryClient()
const router = useRouter()
const mutation = useMutation<AxiosResponse<IrregularityFileResponse>, AxiosError<ApiErrors>>({
mutationFn: deleteIrregularityFile(file.id),
onError: () => AlertStore.show(t('admin.alerts.error'), 'error'),
onSuccess: () => {
AlertStore.show(t('admin.alerts.delete-file'), 'success')
queryClient.invalidateQueries(endpoints.irregularity.viewIrregularity(irregularityId).url)
router.push(routes.admin.irregularity.index)
},
})
const deleteFileHandler = () => {
mutation.mutate()
}
return (
<ListItem key={file.id}>
<ListItemAvatar>
<Avatar>
<FilePresentIcon />
</Avatar>
</ListItemAvatar>
<ListItemText primary={file.filename} />
<></>
<Tooltip
title={
'Note: This link is public on the API side! Need to correct before official release!'
}>
<Button>
{/* TODO: to be discussed. Tracked in issue: https://github.com/podkrepi-bg/frontend/issues/811 */}
<a style={{ color: 'red' }} href={API_URL + `/irregularity-file/${file.id}`}>
{t('admin.cta.download') + '*'}
</a>
</Button>
</Tooltip>
<Button onClick={deleteFileHandler}>{t('admin.cta.delete')}</Button>
</ListItem>
)
}
Example #9
Source File: Dashboard.tsx From NekoMaid with MIT License | 5 votes |
Players: React.FC<{ players?: CurrentStatus['players'] }> = React.memo(({ players }) => {
const his = useHistory()
const plugin = usePlugin()
const globalData = useGlobalData()
const [page, setPage] = useState(1)
const [id, update] = useState(0)
return <Card>
<CardHeader title={lang.dashboard.onlinePlayers} />
<Divider />
<CardContent>
{players?.length === 0
? <Empty />
: <>
<List sx={{ paddingTop: 0 }}>
{players
? players.slice((page - 1) * 8, page * 8).map(p => {
const name = typeof p === 'string' ? p : p.name
return <Tooltip key={name} title={'IP: ' + ((p as any).ip || lang.unknown)}>
<ListItem
secondaryAction={<>
<IconButton
edge='end'
size='small'
onClick={() => dialog(lang.dashboard.confirmKick(<span className='bold'>{name}</span>), lang.reason)
.then(it => it != null && plugin.emit('dashboard:kick', (res: boolean) => {
action(res)
if (!players) return
players.splice(players.indexOf(it!), 1)
update(id + 1)
}, name, it || null))
}
><ExitToApp /></IconButton>
<IconButton edge='end' onClick={() => his.push('/NekoMaid/playerList/' + name)} size='small'><MoreHoriz /></IconButton>
</>
}
>
<ListItemAvatar>
<Avatar
src={getSkin(globalData, name, true)}
imgProps={{ crossOrigin: 'anonymous', onClick () { his.push('/NekoMaid/playerList/' + name) }, style: { width: 40, height: 40 } }}
sx={{ cursor: 'pointer' }}
variant='rounded'
/>
</ListItemAvatar>
<ListItemText primary={name} />
</ListItem>
</Tooltip>
})
: <LoadingList />
}
</List>
{players && <Pagination
page={page}
onChange={(_, it) => setPage(it)}
count={Math.max(Math.ceil(players.length / 8), 1)}
sx={{ display: 'flex', justifyContent: 'flex-end' }}
/>}
</>}
</CardContent>
</Card>
})
Example #10
Source File: PrivateMenu.tsx From frontend with MIT License | 5 votes |
export default function PrivateMenu() {
const { t } = useTranslation()
const { data: session, status } = useSession()
const [anchorEl, setAnchorEl] = useState<Element | null>(null)
const handleMenu = (event: React.MouseEvent) => setAnchorEl(event.currentTarget)
const handleClose = () => setAnchorEl(null)
if (!session) {
return null
}
const title = `${session.name}\n(${session.email})`
return (
<StyledGrid item>
<IconButton onClick={handleMenu} size="large">
{session?.user?.picture ? (
<Avatar title={title} alt={title} src={session?.user?.picture} />
) : (
<AccountCircle sx={{ fill: theme.palette.info.light }} />
)}
</IconButton>
<Menu
keepMounted
id="menu-appbar"
anchorEl={anchorEl}
onClose={handleClose}
open={Boolean(anchorEl)}
anchorOrigin={{ vertical: 'bottom', horizontal: 'right' }}
transformOrigin={{ vertical: 'top', horizontal: 'right' }}>
<LinkMenuItem href={routes.profile.index} className={classes.dropdownLinkText}>
<Typography variant="button">{t('nav.profile')}</Typography>
</LinkMenuItem>
{status === 'authenticated' && isAdmin(session) && (
<LinkMenuItem href={routes.admin.index} className={classes.dropdownLinkText}>
<Typography variant="button">{t('nav.admin.index')}</Typography>
</LinkMenuItem>
)}
<LinkMenuItem href={routes.logout} className={classes.dropdownLinkText}>
<Typography variant="button">{t('nav.logout')}</Typography>
</LinkMenuItem>
</Menu>
</StyledGrid>
)
}
Example #11
Source File: GroupAvatar.tsx From airmessage-web with Apache License 2.0 | 5 votes |
function PersonAvatar(props: {person?: PersonData, className?: string, style?: React.CSSProperties}) {
return <Avatar alt={props.person?.name} src={props.person?.avatar} className={props.className} style={props.style} />;
}
Example #12
Source File: DetailCreate.tsx From airmessage-web with Apache License 2.0 | 5 votes |
function SelectionChip(props: {selection: SelectionData, label: string, tooltip?: string, onDelete: () => void, className?: string}) {
const chip = <Chip className={props.className} label={props.label} avatar={<Avatar src={props.selection.avatar} alt={props.selection.name} />} onDelete={props.onDelete} />;
return !props.tooltip ? chip : <Tooltip title={props.tooltip}>{chip}</Tooltip>;
}
Example #13
Source File: Deposit.tsx From wrap.scrt.network with MIT License | 4 votes |
export default function Deposit({
token,
secretAddress,
onSuccess,
onFailure,
}: {
token: Token;
secretAddress: string;
onSuccess: (txhash: string) => any;
onFailure: (error: any) => any;
}) {
const [sourceAddress, setSourceAddress] = useState<string>("");
const [availableBalance, setAvailableBalance] = useState<string>("");
const [loadingTx, setLoading] = useState<boolean>(false);
const [sourceCosmJs, setSourceCosmJs] =
useState<SigningStargateClient | null>(null);
const [selectedChainIndex, setSelectedChainIndex] = useState<number>(0);
const [fetchBalanceInterval, setFetchBalanceInterval] = useState<any>(null);
const inputRef = useRef<any>();
const maxButtonRef = useRef<any>();
const sourceChain =
chains[token.deposits[selectedChainIndex].source_chain_name];
const targetChain = chains["Secret Network"];
const fetchSourceBalance = async (sourceAddress: string) => {
const url = `${
chains[token.deposits[selectedChainIndex].source_chain_name].lcd
}/bank/balances/${sourceAddress}`;
try {
const response = await fetch(url);
const result: {
height: string;
result: Array<{ denom: string; amount: string }>;
} = await response.json();
const balance =
result.result.find(
(c) => c.denom === token.deposits[selectedChainIndex].from_denom
)?.amount || "0";
setAvailableBalance(balance);
} catch (e) {
console.error(`Error while trying to query ${url}:`, e);
setAvailableBalance("Error");
}
};
useEffect(() => {
setAvailableBalance("");
if (!sourceAddress) {
return;
}
if (fetchBalanceInterval) {
clearInterval(fetchBalanceInterval);
}
fetchSourceBalance(sourceAddress);
const interval = setInterval(
() => fetchSourceBalance(sourceAddress),
10_000
);
setFetchBalanceInterval(interval);
return () => clearInterval(interval);
}, [sourceAddress]);
useEffect(() => {
(async () => {
while (!window.keplr || !window.getOfflineSignerOnlyAmino) {
await sleep(100);
}
if (["LUNA", "UST"].includes(token.name.toUpperCase())) {
await suggestTerraToKeplr(window.keplr);
}
// Initialize cosmjs on the target chain, because it has sendIbcTokens()
const { chain_id, rpc, bech32_prefix } =
chains[token.deposits[selectedChainIndex].source_chain_name];
await window.keplr.enable(chain_id);
const sourceOfflineSigner = window.getOfflineSignerOnlyAmino(chain_id);
const depositFromAccounts = await sourceOfflineSigner.getAccounts();
setSourceAddress(depositFromAccounts[0].address);
const cosmjs = await SigningStargateClient.connectWithSigner(
rpc,
sourceOfflineSigner,
{ prefix: bech32_prefix, broadcastPollIntervalMs: 10_000 }
);
setSourceCosmJs(cosmjs);
})();
}, [selectedChainIndex]);
return (
<>
<div style={{ padding: "1.5em" }}>
<div
style={{
display: "flex",
placeItems: "center",
gap: token.deposits.length === 1 ? "0.3em" : "0.5em",
}}
>
<Typography>
Deposit <strong>{token.name}</strong> from
</Typography>
<If condition={token.deposits.length === 1}>
<Then>
<Typography>
<strong>
{token.deposits[selectedChainIndex].source_chain_name}
</strong>
</Typography>
</Then>
<Else>
<FormControl>
<Select
value={selectedChainIndex}
onChange={(e) =>
setSelectedChainIndex(Number(e.target.value))
}
>
{token.deposits.map((chain, index) => (
<MenuItem value={index} key={index}>
<div
style={{
display: "flex",
gap: "0.5em",
placeItems: "center",
}}
>
<Avatar
src={chains[chain.source_chain_name].chain_image}
sx={{
marginLeft: "0.3em",
width: "1em",
height: "1em",
boxShadow: "rgba(0, 0, 0, 0.15) 0px 6px 10px",
}}
/>
<strong>{chain.source_chain_name}</strong>
</div>
</MenuItem>
))}
</Select>
</FormControl>
</Else>
</If>
<Typography>
to <strong>Secret Network</strong>
</Typography>
</div>
<br />
<div
style={{
display: "flex",
placeContent: "space-between",
placeItems: "center",
gap: "1em",
}}
>
<Typography sx={{ fontWeight: "bold" }}>From:</Typography>
<CopyableAddress
address={sourceAddress}
explorerPrefix={sourceChain.explorer_account}
/>
</div>
<div
style={{
display: "flex",
placeContent: "space-between",
placeItems: "center",
gap: "1em",
}}
>
<Typography sx={{ fontWeight: "bold" }}>To:</Typography>
<CopyableAddress
address={secretAddress}
explorerPrefix={targetChain.explorer_account}
/>
</div>
<br />
<div
style={{
display: "flex",
placeItems: "center",
gap: "0.3em",
marginBottom: "0.8em",
}}
>
<Typography sx={{ fontSize: "0.8em", fontWeight: "bold" }}>
Available to Deposit:
</Typography>
<Typography
sx={{
fontSize: "0.8em",
opacity: 0.8,
cursor: "pointer",
}}
onClick={() => {
maxButtonRef.current.click();
}}
>
{(() => {
if (availableBalance === "") {
return <CircularProgress size="0.6em" />;
}
const prettyBalance = new BigNumber(availableBalance)
.dividedBy(`1e${token.decimals}`)
.toFormat();
if (prettyBalance === "NaN") {
return "Error";
}
return `${prettyBalance} ${token.name}`;
})()}
</Typography>
</div>
<FormControl sx={{ width: "100%" }} variant="standard">
<InputLabel htmlFor="Amount to Deposit">Amount to Deposit</InputLabel>
<Input
autoFocus
id="Amount to Deposit"
fullWidth
type="text"
inputRef={inputRef}
startAdornment={
<InputAdornment position="start">
<Avatar
src={token.image}
sx={{
width: "1em",
height: "1em",
boxShadow: "rgba(0, 0, 0, 0.15) 0px 6px 10px",
}}
/>
</InputAdornment>
}
endAdornment={
<InputAdornment position="end">
<Button
ref={maxButtonRef}
style={{
padding: "0.1em 0.5em",
minWidth: 0,
}}
onClick={() => {
if (availableBalance === "") {
return;
}
const prettyBalance = new BigNumber(availableBalance)
.dividedBy(`1e${token.decimals}`)
.toFormat();
if (prettyBalance === "NaN") {
return;
}
inputRef.current.value = prettyBalance;
}}
>
MAX
</Button>
</InputAdornment>
}
/>
</FormControl>
</div>
<div
style={{
display: "flex",
placeContent: "center",
marginBottom: "0.4em",
}}
>
<LoadingButton
variant="contained"
sx={{
padding: "0.5em 0",
width: "10em",
fontWeight: "bold",
fontSize: "1.2em",
}}
loading={loadingTx}
onClick={async () => {
if (!sourceCosmJs) {
console.error("No cosmjs");
return;
}
if (!inputRef?.current?.value) {
console.error("Empty deposit");
return;
}
const normalizedAmount = (inputRef.current.value as string).replace(
/,/g,
""
);
if (!(Number(normalizedAmount) > 0)) {
console.error(`${normalizedAmount} not bigger than 0`);
return;
}
setLoading(true);
const amount = new BigNumber(normalizedAmount)
.multipliedBy(`1e${token.decimals}`)
.toFixed(0, BigNumber.ROUND_DOWN);
const { deposit_channel_id, deposit_gas } =
chains[token.deposits[selectedChainIndex].source_chain_name];
try {
const { transactionHash } = await sourceCosmJs.sendIbcTokens(
sourceAddress,
secretAddress,
{
amount,
denom: token.deposits[selectedChainIndex].from_denom,
},
"transfer",
deposit_channel_id,
undefined,
Math.floor(Date.now() / 1000) + 15 * 60, // 15 minute timeout
gasToFee(deposit_gas)
);
inputRef.current.value = "";
onSuccess(transactionHash);
} catch (e) {
onFailure(e);
} finally {
setLoading(false);
}
}}
>
Deposit
</LoadingButton>
</div>
</>
);
}
Example #14
Source File: TokenRow.tsx From wrap.scrt.network with MIT License | 4 votes |
export default function TokenRow({
secretjs,
secretAddress,
token,
balances,
loadingCoinBalances,
price,
}: {
secretjs: SecretNetworkClient | null;
secretAddress: string;
loadingCoinBalances: boolean;
token: Token;
balances: Map<string, string>;
price: number;
}) {
const wrapInputRef = useRef<any>();
const [loadingWrap, setLoadingWrap] = useState<boolean>(false);
const [loadingUnwrap, setLoadingUnwrap] = useState<boolean>(false);
const [tokenBalance, setTokenBalance] = useState<string>("");
const [loadingTokenBalance, setLoadingTokenBalance] =
useState<boolean>(false);
const [isDepositWithdrawDialogOpen, setIsDepositWithdrawDialogOpen] =
useState<boolean>(false);
const updateTokenBalance = async () => {
if (!token.address) {
return;
}
if (!secretjs) {
return;
}
const key = await getKeplrViewingKey(token.address);
if (!key) {
setTokenBalance(viewingKeyErrorString);
return;
}
try {
const result = await secretjs.query.compute.queryContract({
address: token.address,
codeHash: token.code_hash,
query: {
balance: { address: secretAddress, key },
},
});
if (result.viewing_key_error) {
setTokenBalance(viewingKeyErrorString);
return;
}
setTokenBalance(result.balance.amount);
} catch (e) {
console.error(`Error getting balance for s${token.name}`, e);
setTokenBalance(viewingKeyErrorString);
}
};
useEffect(() => {
(async () => {
try {
setLoadingTokenBalance(true);
await updateTokenBalance();
} finally {
setLoadingTokenBalance(false);
}
})();
}, [secretjs]);
const denomOnSecret = token.withdrawals[0]?.from_denom;
let balanceIbcCoin;
let balanceToken;
if (token.address) {
if (loadingCoinBalances) {
balanceIbcCoin = (
<div>
<div>
Balance: <CircularProgress size="0.8em" />
</div>
<div style={{ opacity: 0 }}>placeholder</div>
</div>
);
} else if (balances.get(denomOnSecret)) {
balanceIbcCoin = (
<div>
<div
style={{ cursor: "pointer" }}
onClick={() => {
wrapInputRef.current.value = new BigNumber(
balances.get(denomOnSecret)!
)
.dividedBy(`1e${token.decimals}`)
.toFixed();
}}
>
{`Balance: ${new BigNumber(balances.get(denomOnSecret)!)
.dividedBy(`1e${token.decimals}`)
.toFormat()}`}
</div>
<div style={{ display: "flex", opacity: 0.7 }}>
{usdString.format(
new BigNumber(balances.get(denomOnSecret)!)
.dividedBy(`1e${token.decimals}`)
.multipliedBy(price)
.toNumber()
)}
</div>
</div>
);
} else {
balanceIbcCoin = (
<div>
<div
style={{ cursor: "pointer" }}
onClick={() => {
document.getElementById("keplr-button")?.click();
}}
>
connect wallet
</div>
<div style={{ opacity: 0 }}>(please)</div>
</div>
);
}
} else {
balanceIbcCoin = (
<div>
<div>coming soon</div>
<div>(?)</div>
</div>
);
}
if (token.address) {
if (!secretjs) {
balanceToken = (
<div>
<div
style={{ cursor: "pointer" }}
onClick={() => {
document.getElementById("keplr-button")?.click();
}}
>
connect wallet
</div>
<div style={{ opacity: 0 }}>(please)</div>
</div>
);
} else if (loadingTokenBalance) {
balanceToken = (
<div>
<div>
Balance: <CircularProgress size="0.8em" />
</div>
<div style={{ opacity: 0 }}>placeholder</div>
</div>
);
} else if (tokenBalance == viewingKeyErrorString) {
balanceToken = (
<div>
<Tooltip title="Set Viewing Key" placement="top">
<div
style={{ cursor: "pointer" }}
onClick={async () => {
await setKeplrViewingKey(token.address);
try {
setLoadingTokenBalance(true);
await sleep(1000); // sometimes query nodes lag
await updateTokenBalance();
} finally {
setLoadingTokenBalance(false);
}
}}
>
{`Balance: ${viewingKeyErrorString}`}
</div>
</Tooltip>
<div style={{ opacity: 0 }}>placeholder</div>
</div>
);
} else if (Number(tokenBalance) > -1) {
balanceToken = (
<div>
<div
style={{ cursor: "pointer" }}
onClick={() => {
wrapInputRef.current.value = new BigNumber(tokenBalance)
.dividedBy(`1e${token.decimals}`)
.toFixed();
}}
>
{`Balance: ${new BigNumber(tokenBalance)
.dividedBy(`1e${token.decimals}`)
.toFormat()}`}
</div>
<div
style={{ display: "flex", placeContent: "flex-end", opacity: 0.7 }}
>
{usdString.format(
new BigNumber(tokenBalance)
.dividedBy(`1e${token.decimals}`)
.multipliedBy(price)
.toNumber()
)}
</div>
</div>
);
}
} else {
balanceToken = (
<div>
<div>coming soon</div>
<div style={{ display: "flex", placeContent: "flex-end" }}>(?)</div>
</div>
);
}
return (
<>
<Breakpoint small down>
<div
style={{
display: "flex",
placeContent: "space-between",
placeItems: "center",
padding: "0.8rem",
fontSize: "1.2rem",
}}
>
<Avatar
src={token.image}
sx={{
width: 44,
height: 44,
boxShadow: "rgba(0, 0, 0, 0.15) 0px 6px 10px",
}}
/>
<div>{token.name}</div>
<div style={{ display: "flex" }}>
<If condition={token.address}>
<Then>
<CompareArrowsIcon style={{ transform: "scale(0.9)" }} />
</Then>
<Else>
<div style={{ fontSize: "0.65rem" }}>(soon™)</div>
</Else>
</If>
</div>
<div>s{token.name}</div>
<Avatar
src={token.image}
sx={{
width: 44,
height: 44,
boxShadow: "rgba(0, 0, 0, 0.15) 0px 6px 10px",
}}
/>
</div>
</Breakpoint>
<Breakpoint medium up>
<div
style={{
display: "flex",
placeItems: "center",
gap: "0.8rem",
padding: "0.4rem",
borderRadius: 20,
}}
>
<span style={{ flex: 1 }}></span>
<Avatar
src={token.image}
sx={{
width: 38,
height: 38,
boxShadow: "rgba(0, 0, 0, 0.15) 0px 6px 10px",
}}
/>
<div
style={{
display: "flex",
width: 150,
}}
>
<div
style={{
display: "flex",
flexDirection: "column",
placeItems: "flex-start",
}}
>
<div
style={{
display: "flex",
placeContent: "space-between",
placeItems: "center",
minWidth: "5.6em",
}}
>
<span>{token.name}</span>
<When condition={token.address && secretAddress}>
<>
<Tooltip title={`IBC Deposit & Withdraw`} placement="top">
<Button
style={{
minWidth: 0,
padding: 0,
marginBottom: "0.2em",
color: "black",
opacity: 0.8,
}}
onClick={async () =>
setIsDepositWithdrawDialogOpen(true)
}
>
<CompareArrowsIcon sx={{ height: "0.8em" }} />
</Button>
</Tooltip>
<DepositWithdrawDialog
token={token}
balances={balances}
secretAddress={secretAddress}
secretjs={secretjs}
isOpen={isDepositWithdrawDialogOpen}
setIsOpen={setIsDepositWithdrawDialogOpen}
/>
</>
</When>
</div>
<span style={{ fontSize: "0.75rem" }}>{balanceIbcCoin}</span>
</div>
</div>
<div
style={{
display: "flex",
placeItems: "center",
gap: "0.3rem",
}}
>
<Button
disabled={token.address === ""}
size="small"
variant="text"
startIcon={
<If condition={loadingUnwrap}>
<Then>
<CircularProgress size="0.8em" />
</Then>
<Else>
<KeyboardArrowLeftIcon />
</Else>
</If>
}
onClick={async () => {
if (
!secretjs ||
!secretAddress ||
loadingWrap ||
loadingUnwrap
) {
return;
}
const baseAmount = wrapInputRef?.current?.value;
const amount = new BigNumber(baseAmount)
.multipliedBy(`1e${token.decimals}`)
.toFixed(0, BigNumber.ROUND_DOWN);
if (amount === "NaN") {
console.error("NaN amount", baseAmount);
return;
}
setLoadingUnwrap(true);
try {
const tx = await secretjs.tx.broadcast(
[
new MsgExecuteContract({
sender: secretAddress,
contract: token.address,
codeHash: token.code_hash,
sentFunds: [],
msg: {
redeem: {
amount,
denom:
token.name === "SCRT"
? undefined
: token.withdrawals[0].from_denom,
},
},
}),
],
{
gasLimit: 40_000,
gasPriceInFeeDenom: 0.25,
feeDenom: "uscrt",
}
);
if (tx.code === 0) {
wrapInputRef.current.value = "";
console.log(`Unwrapped successfully`);
} else {
console.error(`Tx failed: ${tx.rawLog}`);
}
} finally {
setLoadingUnwrap(false);
try {
setLoadingTokenBalance(true);
await sleep(1000); // sometimes query nodes lag
await updateTokenBalance();
} finally {
setLoadingTokenBalance(false);
}
}
}}
>
<Breakpoint medium up>
Unwrap
</Breakpoint>
</Button>
<Input
disabled={token.address === ""}
// TODO add input validation
placeholder="Amount"
inputProps={{
style: {
textAlign: "center",
textOverflow: "ellipsis",
},
}}
inputRef={wrapInputRef}
/>
<Button
disabled={token.address === ""}
size="small"
variant="text"
endIcon={
loadingWrap ? (
<CircularProgress size="0.8em" />
) : (
<KeyboardArrowRightIcon />
)
}
onClick={async () => {
if (
!secretjs ||
!secretAddress ||
loadingWrap ||
loadingUnwrap
) {
return;
}
const baseAmount = wrapInputRef?.current?.value;
const amount = new BigNumber(baseAmount)
.multipliedBy(`1e${token.decimals}`)
.toFixed(0, BigNumber.ROUND_DOWN);
if (amount === "NaN") {
console.error("NaN amount", baseAmount);
return;
}
setLoadingWrap(true);
try {
const tx = await secretjs.tx.broadcast(
[
new MsgExecuteContract({
sender: secretAddress,
contract: token.address,
codeHash: token.code_hash,
sentFunds: [
{ denom: token.withdrawals[0].from_denom, amount },
],
msg: { deposit: {} },
}),
],
{
gasLimit: 40_000,
gasPriceInFeeDenom: 0.25,
feeDenom: "uscrt",
}
);
if (tx.code === 0) {
wrapInputRef.current.value = "";
console.log(`Wrapped successfully`);
} else {
console.error(`Tx failed: ${tx.rawLog}`);
}
} finally {
setLoadingWrap(false);
try {
setLoadingTokenBalance(true);
await sleep(1000); // sometimes query nodes lag
await updateTokenBalance();
} finally {
setLoadingTokenBalance(false);
}
}
}}
>
<Breakpoint medium up>
Wrap
</Breakpoint>
</Button>
</div>
<div
style={{
display: "flex",
width: 150,
placeContent: "flex-end",
}}
>
<div
style={{
display: "flex",
flexDirection: "column",
placeItems: "flex-end",
}}
>
<span>s{token.name}</span>
<div
style={{
fontSize: "0.75rem",
display: "flex",
placeItems: "flex-start",
gap: "0.2em",
}}
>
<span>{balanceToken}</span>
<When condition={token.address && secretAddress}>
<Tooltip title="Refresh Balance" placement="top">
<Button
style={{
color: "black",
minWidth: 0,
padding: 0,
display: loadingTokenBalance ? "none" : undefined,
}}
onClick={async () => {
try {
setLoadingTokenBalance(true);
await updateTokenBalance();
} finally {
setLoadingTokenBalance(false);
}
}}
>
<RefreshIcon sx={{ height: "0.7em" }} />
</Button>
</Tooltip>
</When>
</div>
</div>
</div>
<Avatar
src={token.image}
sx={{
width: 38,
height: 38,
boxShadow: "rgba(0, 0, 0, 0.15) 0px 6px 10px",
}}
/>
<span style={{ flex: 1 }}></span>
</div>
</Breakpoint>
</>
);
}
Example #15
Source File: Withdraw.tsx From wrap.scrt.network with MIT License | 4 votes |
export default function Withdraw({
token,
secretjs,
secretAddress,
balances,
onSuccess,
onFailure,
}: {
token: Token;
secretjs: SecretNetworkClient | null;
secretAddress: string;
balances: Map<string, string>;
onSuccess: (txhash: string) => any;
onFailure: (error: any) => any;
}) {
const [targetAddress, setTargetAddress] = useState<string>("");
const [loadingTx, setLoading] = useState<boolean>(false);
const [selectedChainIndex, setSelectedChainIndex] = useState<number>(0);
const inputRef = useRef<any>();
const maxButtonRef = useRef<any>();
const sourceChain = chains["Secret Network"];
const targetChain =
chains[token.withdrawals[selectedChainIndex].target_chain_name];
const availableBalance =
balances.get(token.withdrawals[selectedChainIndex].from_denom) || "";
useEffect(() => {
(async () => {
while (!window.keplr || !window.getOfflineSignerOnlyAmino) {
await sleep(100);
}
// Find address on target chain
const { chain_id: targetChainId } =
chains[token.withdrawals[selectedChainIndex].target_chain_name];
if (token.withdrawals[selectedChainIndex].target_chain_name === "Terra") {
await suggestTerraToKeplr(window.keplr);
}
await window.keplr.enable(targetChainId);
const targetOfflineSigner =
window.getOfflineSignerOnlyAmino(targetChainId);
const targetFromAccounts = await targetOfflineSigner.getAccounts();
setTargetAddress(targetFromAccounts[0].address);
})();
}, [selectedChainIndex]);
return (
<>
<div style={{ padding: "1.5em" }}>
<div
style={{
display: "flex",
placeItems: "center",
gap: "0.5em",
}}
>
<Typography>
Withdraw <strong>{token.name}</strong> from{" "}
<strong>Secret Network</strong> to
</Typography>
<If condition={token.withdrawals.length === 1}>
<Then>
<Typography sx={{ marginLeft: "-0.2em" }}>
<strong>
{token.withdrawals[selectedChainIndex].target_chain_name}
</strong>
</Typography>
</Then>
<Else>
<FormControl>
<Select
value={selectedChainIndex}
onChange={(e) =>
setSelectedChainIndex(Number(e.target.value))
}
>
{token.withdrawals.map((chain, index) => (
<MenuItem value={index} key={index}>
<div
style={{
display: "flex",
gap: "0.5em",
placeItems: "center",
}}
>
<Avatar
src={chains[chain.target_chain_name].chain_image}
sx={{
marginLeft: "0.3em",
width: "1em",
height: "1em",
boxShadow: "rgba(0, 0, 0, 0.15) 0px 6px 10px",
}}
/>
<strong>{chain.target_chain_name}</strong>
</div>
</MenuItem>
))}
</Select>
</FormControl>
</Else>
</If>
</div>
<br />
<div
style={{
display: "flex",
placeContent: "space-between",
placeItems: "center",
gap: "1em",
}}
>
<Typography sx={{ fontWeight: "bold" }}>From:</Typography>
<CopyableAddress
address={secretAddress}
explorerPrefix={sourceChain.explorer_account}
/>
</div>
<div
style={{
display: "flex",
placeContent: "space-between",
placeItems: "center",
gap: "1em",
}}
>
<Typography sx={{ fontWeight: "bold" }}>To:</Typography>
<CopyableAddress
address={targetAddress}
explorerPrefix={targetChain.explorer_account}
/>
</div>
<br />
<div
style={{
display: "flex",
placeItems: "center",
gap: "0.3em",
marginBottom: "0.8em",
}}
>
<Typography sx={{ fontSize: "0.8em", fontWeight: "bold" }}>
Available to Withdraw:
</Typography>
<Typography
sx={{
fontSize: "0.8em",
opacity: 0.8,
cursor: "pointer",
}}
onClick={() => {
maxButtonRef.current.click();
}}
>
{(() => {
if (availableBalance === "") {
return <CircularProgress size="0.6em" />;
}
const prettyBalance = new BigNumber(availableBalance)
.dividedBy(`1e${token.decimals}`)
.toFormat();
if (prettyBalance === "NaN") {
return "Error";
}
return `${prettyBalance} ${token.name}`;
})()}
</Typography>
</div>
<FormControl sx={{ width: "100%" }} variant="standard">
<InputLabel htmlFor="Amount to Withdraw">
Amount to Withdraw
</InputLabel>
<Input
autoFocus
id="Amount to Withdraw"
fullWidth
type="text"
inputRef={inputRef}
startAdornment={
<InputAdornment position="start">
<Avatar
src={token.image}
sx={{
width: "1em",
height: "1em",
boxShadow: "rgba(0, 0, 0, 0.15) 0px 6px 10px",
}}
/>
</InputAdornment>
}
endAdornment={
<InputAdornment position="end">
<Button
ref={maxButtonRef}
style={{
padding: "0.1em 0.5em",
minWidth: 0,
}}
onClick={() => {
if (availableBalance === "") {
return;
}
const prettyBalance = new BigNumber(availableBalance)
.dividedBy(`1e${token.decimals}`)
.toFormat();
if (prettyBalance === "NaN") {
return;
}
inputRef.current.value = prettyBalance;
}}
>
MAX
</Button>
</InputAdornment>
}
/>
</FormControl>
</div>
<div
style={{
display: "flex",
placeContent: "center",
marginBottom: "0.4em",
}}
>
<LoadingButton
variant="contained"
sx={{
padding: "0.5em 0",
width: "10em",
fontWeight: "bold",
fontSize: "1.2em",
}}
loading={loadingTx}
onClick={async () => {
if (!secretjs) {
console.error("No secretjs");
return;
}
if (!inputRef?.current?.value) {
console.error("Empty withdraw");
return;
}
const normalizedAmount = (inputRef.current.value as string).replace(
/,/g,
""
);
if (!(Number(normalizedAmount) > 0)) {
console.error(`${normalizedAmount} not bigger than 0`);
return;
}
setLoading(true);
const amount = new BigNumber(normalizedAmount)
.multipliedBy(`1e${token.decimals}`)
.toFixed(0, BigNumber.ROUND_DOWN);
const { withdraw_channel_id, withdraw_gas } =
chains[token.withdrawals[selectedChainIndex].target_chain_name];
try {
const tx = await secretjs.tx.broadcast(
[
new MsgTransfer({
sender: secretAddress,
receiver: targetAddress,
sourceChannel: withdraw_channel_id,
sourcePort: "transfer",
token: {
amount,
denom: token.withdrawals[selectedChainIndex].from_denom,
},
timeoutTimestampSec: String(
Math.floor(Date.now() / 1000) + 15 * 60
), // 15 minute timeout
}),
],
{
gasLimit: withdraw_gas,
gasPriceInFeeDenom: 0.25,
feeDenom: "uscrt",
}
);
if (tx.code === 0) {
inputRef.current.value = "";
onSuccess(tx.transactionHash);
} else {
onFailure(tx.rawLog);
}
} catch (e) {
onFailure(e);
} finally {
setLoading(false);
}
}}
>
Withdraw
</LoadingButton>
</div>
</>
);
}
Example #16
Source File: siteCard.tsx From Search-Next with GNU General Public License v3.0 | 4 votes |
SiteCard: React.FC<SiteCardPropTypes> = ({
type = 'view',
item,
onClick,
onAdd,
onEdit,
onRemove,
}) => {
const [anchorEl, setAnchorEl] = React.useState<null | HTMLElement>(null);
const onMenuOpen = (event: React.MouseEvent<HTMLButtonElement>) => {
event.stopPropagation();
setAnchorEl(event.currentTarget);
};
const onMenuClose = (event: React.MouseEvent<HTMLButtonElement>) => {
event.stopPropagation();
setAnchorEl(null);
};
const siteClick = () => {
if (onClick) onClick();
};
const handleEdit = () => {
setAnchorEl(null);
if (onEdit && item) onEdit(item);
};
const handleRemove = () => {
setAnchorEl(null);
if (onRemove && item) onRemove(item._id);
};
return (
<>
<div
className={classNames(
'cursor-pointer bg-transparent rounded shadow-none relative w-28 h-20 transition-all hover:bg-rgba-gray-3',
css`
&:hover {
.handle-container {
opacity: 1 !important;
}
}
`,
)}
onClick={onClick}
>
<div
className={classNames([
'content-container transition-all box-border flex flex-col items-center w-full h-full px-3 py-2 rounded',
type === 'add' ? 'justify-start text-primary' : 'justify-end',
])}
>
<Avatar
className={classNames(
'bg-white rounded mx-auto',
{
'text-primary': type === 'add',
},
css`
img {
max-width: 24px;
height: 24px;
}
`,
)}
variant="rounded"
src={item?.iconUrl ? item?.iconUrl : getWebIconByUrl(item?.url)}
onClick={onAdd ? onAdd : undefined}
>
{type === 'add' && <AddOutlined />}
</Avatar>
<p className="text-center flex justify-center mt-1 w-full text-var-main-10">
<a className="overflow-hidden text-ellipsis">{item?.name}</a>
</p>
</div>
{type === 'view' && (
<span className="handle-container w-6 h-6 flex justify-center absolute top-1 right-1 z-10 opacity-0 transition-all">
<IconButton
className="rounded"
size="small"
aria-controls={`site-${item?.name}-menu`}
aria-haspopup="true"
onClick={onMenuOpen}
>
<MoreHorizOutlined className="text-var-main-10" />
</IconButton>
</span>
)}
</div>
<Menu
id={`site-${item?.name}-menu`}
open={Boolean(anchorEl)}
anchorEl={anchorEl}
onClose={onMenuClose}
config={[
{
label: '修改',
onClick: handleEdit,
icon: EditOutlined,
},
{
label: '删除',
onClick: handleRemove,
icon: DeleteOutlineOutlined,
},
]}
/>
</>
);
}
Example #17
Source File: websiteCardNew.tsx From Search-Next with GNU General Public License v3.0 | 4 votes |
WebsiteCardNew: React.FC<WebsiteCardNewProps> = (props) => {
const { datasource } = props;
const { name, intro, color, url } = datasource;
const onAdd = () => {
const res = addSite({
name,
url: url.substring(0, url.lastIndexOf('/')),
});
if (res) toast.success('添加成功');
};
const onCopy = () => {
if (navigator.clipboard) {
navigator.clipboard.writeText(url);
toast.success(`已复制 ${name} (${url})`);
} else {
const copy = new Clipboard(`.copy-button_${name}`);
copy.on('success', (e) => {
toast.success(`已复制 ${name} (${url})`);
});
copy.on('error', function (e) {
toast.warning(
`您的浏览器不支持复制功能,请点击跳转到该网站手动复制地址`,
);
});
}
};
const onMore = () => {
toast.warning('功能开发中...');
};
return (
<div
className={classNames(
'cursor-pointer shadow-md rounded border-b-2',
css`
--tw-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1),
0 1px 3px 0 ${hexToRgba(color ?? '#000', 0.45).rgba} !important;
border-bottom-color: ${color};
`,
)}
>
<CardActionArea>
<Tooltip title={intro || '暂无介绍'}>
<div className="p-3 flex gap-3" onClick={() => window.open(url)}>
<Avatar
// style={{ backgroundColor: color }}
src={getWebIconByUrl(url)}
>
{name.split('')[0].toUpperCase()}
</Avatar>
<div className="flex-grow overflow-hidden">
<p className="font-bold text-base whitespace-nowrap overflow-x-hidden">
{name}
</p>
<Overflow>{(intro as any) || ('暂无介绍' as any)}</Overflow>
</div>
</div>
</Tooltip>
</CardActionArea>
<div>
<ButtonGroup
disableElevation
variant="text"
size="small"
className={classNames(
'w-full h-full flex',
css`
justify-content: flex-end;
button {
height: 100%;
border-right: 0px !important;
}
`,
)}
>
<Tooltip title="添加到首页">
<Button onClick={onAdd}>
<Add />
</Button>
</Tooltip>
<Tooltip title="复制网站链接">
<Button
className={`copy-button_${name}`}
data-clipboard-text={url}
onClick={onCopy}
>
<CopyAll />
</Button>
</Tooltip>
{false && (
<Tooltip title="更多">
<Button onClick={onMore}>
<MoreHoriz />
</Button>
</Tooltip>
)}
</ButtonGroup>
</div>
</div>
);
}
Example #18
Source File: PlayerList.tsx From NekoMaid with MIT License | 4 votes |
Players: React.FC = () => {
const his = useHistory()
const plugin = usePlugin()
const [page, setPage] = useState(0)
const [loading, setLoading] = useState(true)
const [state, setState] = useState<number | null>(null)
const [activedPlayer, setActivedPlayer] = useState<PlayerData | null>(null)
const [anchorEl, setAnchorEl] = React.useState<HTMLButtonElement | null>(null)
const [data, setData] = useState<{ count: number, players: PlayerData[] }>(() => ({ count: 0, players: [] }))
const globalData = useGlobalData()
const { hasWhitelist } = globalData
const refresh = () => {
setLoading(true)
plugin.emit('playerList:fetchPage', (it: any) => {
if (it.players == null) it.players = []
setData(it)
setLoading(false)
}, page, state === 1 || state === 2 ? state : 0, null)
}
useMemo(refresh, [page, state])
const close = () => {
setAnchorEl(null)
setActivedPlayer(null)
}
return <Card>
<CardHeader
title={lang.playerList.title}
action={
<ToggleButtonGroup
size='small'
color={(state === 1 ? 'warning' : state === 2 ? 'error' : undefined) as any}
value={state}
exclusive
onChange={(_, it) => {
if (it === 3) return
setState(it)
if (state === 3) refresh()
}}
>
<ToggleButton disabled={loading} value={1}><Star /></ToggleButton>
<ToggleButton disabled={loading} value={2}><Block /></ToggleButton>
<ToggleButton disabled={loading} value={3} onClick={() => state !== 3 && dialog(lang.playerList.nameToSearch, lang.username)
.then(filter => {
if (filter == null) return
his.push('/NekoMaid/playerList/' + filter)
setState(3)
setLoading(true)
plugin.emit('playerList:fetchPage', (it: any) => {
if (it.players == null) it.players = []
setPage(0)
setData(it)
setLoading(false)
}, page, 0, filter.toLowerCase())
})}><Search /></ToggleButton>
</ToggleButtonGroup>
}
/>
<Divider />
<Box sx={{ position: 'relative' }}>
<CircularLoading loading={loading} />
<TableContainer>
<Table>
<TableHead>
<TableRow>
<TableCell padding='checkbox' />
<TableCell>{lang.username}</TableCell>
<TableCell align='right'>{minecraft['stat.minecraft.play_time']}</TableCell>
<TableCell align='right'>{lang.playerList.lastPlay}</TableCell>
<TableCell align='right'>{lang.operations}</TableCell>
</TableRow>
</TableHead>
<TableBody>
{data.players.map(it => <TableRow key={it.name}>
<TableCell sx={{ cursor: 'pointer', padding: theme => theme.spacing(1, 1, 1, 2) }} onClick={() => his.push('/NekoMaid/playerList/' + it.name)}>
<Avatar src={getSkin(globalData, it.name, true)} imgProps={{ crossOrigin: 'anonymous', style: { width: 40, height: 40 } }} variant='rounded' />
</TableCell>
<TableCell>{it.name}</TableCell>
<TableCell align='right'>{dayjs.duration(it.playTime / 20, 'seconds').humanize()}</TableCell>
<TableCell align='right'>{dayjs(it.lastOnline).fromNow()}</TableCell>
<TableCell align='right'>
{(state === 1 || hasWhitelist) && <Tooltip title={lang.playerList[it.whitelisted ? 'clickToRemoveWhitelist' : 'clickToAddWhitelist']}>
<IconButton onClick={() => whitelist(it.name, plugin, refresh, !it.whitelisted)}>
{it.whitelisted ? <Star color='warning' /> : <StarBorder />}
</IconButton>
</Tooltip>}
<Tooltip title={it.ban == null ? lang.playerList.clickToBan : lang.playerList.banned + ': ' + it.ban}>
<IconButton onClick={() => banPlayer(it.name, plugin, refresh, it.ban == null)}>
<Block color={it.ban == null ? undefined : 'error'} />
</IconButton>
</Tooltip>
{actions.length
? <IconButton onClick={e => {
setActivedPlayer(anchorEl ? null : it)
setAnchorEl(anchorEl ? null : e.currentTarget)
}}><MoreHoriz /></IconButton>
: null}
</TableCell>
</TableRow>)}
</TableBody>
</Table>
</TableContainer>
<TablePagination
rowsPerPageOptions={[]}
component='div'
count={data.count}
rowsPerPage={10}
page={page}
onPageChange={(_, it) => !loading && setPage(it)}
/>
</Box>
<Menu
anchorEl={anchorEl}
open={Boolean(anchorEl)}
onClose={() => setAnchorEl(null)}
>{actions.map((It, i) => <It key={i} onClose={close} player={activedPlayer} />)}</Menu>
</Card>
}
Example #19
Source File: DonationTable.tsx From frontend with MIT License | 4 votes |
function DonationTable({ donations }: DonationTableProps) {
const { t, i18n } = useTranslation()
const [fromDate, setFromDate] = React.useState<Date | null>(null)
const [toDate, setToDate] = React.useState<Date | null>(null)
const [monthly, setMonthly] = React.useState(true)
const [oneTime, setOneTime] = React.useState(true)
const filteredByTypeDonations = useMemo(() => {
if (monthly && oneTime) {
return donations
}
if (!monthly && !oneTime) {
return []
}
if (monthly) {
return donations?.filter((d) => d.type !== 'donation')
}
if (oneTime) {
return donations?.filter((d) => d.type === 'donation')
}
return donations
}, [donations, monthly, oneTime])
const filteredDonations = useMemo(() => {
if (!fromDate && !toDate) {
return filteredByTypeDonations
}
if (fromDate && toDate) {
return filteredByTypeDonations?.filter((d) => {
const createdAtDate = parseISO(d.createdAt)
return isAfter(createdAtDate, fromDate) && isBefore(createdAtDate, toDate)
})
}
if (fromDate) {
return filteredByTypeDonations?.filter((d) => {
const createdAtDate = parseISO(d.createdAt)
return isAfter(createdAtDate, fromDate)
})
}
if (toDate) {
return filteredByTypeDonations?.filter((d) => {
const createdAtDate = parseISO(d.createdAt)
return isBefore(createdAtDate, toDate)
})
}
}, [filteredByTypeDonations, fromDate, toDate])
return (
<Card sx={{ padding: theme.spacing(2) }}>
<Grid container alignItems={'flex-start'} spacing={theme.spacing(2)}>
<Grid item xs={6} sm={3}>
<CheckboxLabel>{t('profile:donations.oneTime')}</CheckboxLabel>
<Checkbox
onChange={(e, checked) => setOneTime(checked)}
checked={oneTime}
name="oneTime"
/>
</Grid>
<Grid item xs={6} sm={3}>
<CheckboxLabel>{t('profile:donations.monthly')}</CheckboxLabel>
<Checkbox
onChange={(e, checked) => setMonthly(checked)}
checked={monthly}
name="monthly"
/>
</Grid>
<LocalizationProvider
locale={i18n.language === 'bg' ? bg : enUS}
dateAdapter={AdapterDateFns}>
<Grid item xs={12} sm={3}>
<DatePicker
label={t('profile:donations.fromDate')}
value={fromDate}
onChange={setFromDate}
renderInput={(params) => <TextField size="small" {...params} />}
/>
</Grid>
<Grid item xs={12} sm={3}>
<DatePicker
label={t('profile:donations.toDate')}
value={toDate}
onChange={setToDate}
renderInput={(params) => <TextField size="small" {...params} />}
/>
</Grid>
</LocalizationProvider>
</Grid>
{filteredDonations?.length ? (
<TableContainer>
<Table sx={{ minWidth: 650, backgroundColor: 'white' }} aria-label="simple table">
<TableHead>
<TableRow>
<TableCell>№</TableCell>
<TableCell>{t('profile:donations.date')}</TableCell>
<TableCell>{t('profile:donations.type')}</TableCell>
<TableCell>{t('profile:donations.cause')}</TableCell>
<TableCell>{t('profile:donations.amount')}</TableCell>
<TableCell>{t('profile:donations.certificate')}</TableCell>
</TableRow>
</TableHead>
<TableBody>
{filteredDonations.map((donation, index) => (
<TableRow key={index} sx={{ '&:last-child td, &:last-child th': { border: 0 } }}>
<TableCell component="th" scope="row">
{index + 1}
</TableCell>
<TableCell>
{format(parseISO(donation.createdAt), 'd.LL.yyyy', {
locale: i18n.language === 'bg' ? bg : enUS,
})}
</TableCell>
<TableCell>
<Avatar sx={{ background: darken(theme.palette.secondary.main, 0.175) }}>
<StarIcon />
</Avatar>
</TableCell>
<TableCell>{donation.targetVault.campaign.title}</TableCell>
<TableCell>{money(donation.amount)}</TableCell>
<TableCell>
<Button variant="outlined" disabled={donation.status != 'succeeded'}>
<Link target="_blank" href={routes.donation.viewCertificate(donation.id)}>
{t('profile:donations.download')} <ArrowForwardIcon />
</Link>
</Button>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
) : (
<Box sx={{ fontSize: 20, mt: 4 }}>Към момента няма направени дарения</Box>
)}
</Card>
)
}
Example #20
Source File: Vault.tsx From NekoMaid with MIT License | 4 votes |
Vault: React.FC = () => { const his = useHistory() const plugin = usePlugin() const globalData = useGlobalData() const { hasVaultPermission, hasVaultChat, vaultEconomy, hasVaultGroups } = globalData const [players, setPlayers] = useState<PlayerInfo[]>([]) const [count, setCount] = useState(-1) const [page, setPage] = useState(0) const [sortModel, setSortModel] = useState<GridSortItem[]>([]) const [groups, setGroups] = useState<GroupInfo[]>([]) const [selectedId, setSelectedId] = useState<string | undefined>() const [selectedPlayer, setSelectedPlayer] = useState<string | undefined>() const [isGroup, setIsGroup] = useState(false) const balanceSort = sortModel[0]?.sort const refresh = (res?: boolean) => { if (res != null) action(res) setCount(-1) plugin.emit('vault:fetch', (a, b) => { setCount(a) setPlayers(b) }, page, balanceSort) } useEffect(refresh, [page, balanceSort]) useEffect(() => { plugin.emit('vault:fetchGroups', setGroups) }, []) const columns: any[] = [ { field: '', sortable: false, width: 60, renderCell: (it: GridCellParams) => <Avatar src={getSkin(globalData, it.id, true)} imgProps={{ crossOrigin: 'anonymous', onClick () { his.push('/NekoMaid/playerList/' + it.id) }, style: { width: 40, height: 40 } }} variant='rounded' sx={{ cursor: 'pointer' }} /> }, { field: 'id', headerName: lang.username, sortable: false, width: 200 } ] const columns2: any[] = [ { field: 'id', headerName: lang.vault.groupName, sortable: false, width: 160 } ] if (hasVaultGroups) { columns.push({ field: 'group', headerName: lang.vault.defaultGroup, width: 110, sortable: false }) } if (hasVaultChat) { const a = { field: 'prefix', headerName: lang.vault.prefix, width: 110, editable: true, sortable: false } const b = { field: 'suffix', headerName: lang.vault.suffix, width: 110, editable: true, sortable: false } columns.push(a, b) columns2.push(a, b) } if (vaultEconomy) { columns.push({ field: 'balance', headerName: lang.vault.balance, editable: true, width: 110, valueFormatter: ({ value }: any) => (value === 0 || value === 1 ? vaultEconomy.singular : vaultEconomy.plural) + (vaultEconomy.digits === -1 ? value : value.toFixed(vaultEconomy.digits)) }) } if (hasVaultPermission) { columns.push({ field: '_', headerName: lang.operations, width: 88, sortable: false, renderCell: (it: GridCellParams) => <> {hasVaultGroups && <Tooltip title={lang.vault.managePermissionGroup}> <IconButton onClick={() => setSelectedPlayer(it.id as any)} size='small'><GroupsIcon /></IconButton> </Tooltip>} <Tooltip title={lang.vault.managePermission}><IconButton onClick={() => { setSelectedId(it.id as any) setIsGroup(false) }} size='small'><ListIcon /></IconButton></Tooltip> </> }) if (hasVaultGroups) { columns2.push({ field: '_', headerName: lang.operations, width: 66, sortable: false, renderCell: (it: GridCellParams) => <Tooltip title={lang.vault.managePermission}><IconButton onClick={() => { setSelectedId(it.id as any) setIsGroup(true) }} size='small'><ListIcon /></IconButton></Tooltip> }) } } const playerList = <Card> <CardHeader title={lang.playerList.title} action={<IconButton onClick={() => dialog(lang.playerList.nameToSearch, lang.username).then(filter => { if (!filter) return refresh() setCount(-1) plugin.emit('vault:fetch', (a, b) => { setCount(a) setPlayers(b) success() }, page, sortModel.find(it => it.field === 'balance'), filter.toLowerCase()) })} ><Search /></IconButton>} /> <Divider /> <div style={{ height: 594, width: '100%' }}> <DataGrid pagination disableColumnMenu hideFooterSelectedRowCount rows={players} columns={columns} pageSize={10} rowCount={count === -1 ? 0 : count} loading={count === -1} onPageChange={setPage} paginationMode='server' sortingMode='server' sortModel={sortModel} onSortModelChange={it => !isEqual(sortModel, it) && setSortModel(it)} onCellEditCommit={({ field, id, value }) => { let flag = false switch (field) { case 'balance': if (isNaN(+value!)) refresh() else plugin.emit('vault:setBalance', refresh, id, +value!) break case 'prefix': flag = true // eslint-disable-next-line no-fallthrough case 'suffix': plugin.emit('vault:setChat', refresh, id, false, flag, value || null) } }} /> </div> </Card> return <Box sx={{ minHeight: '100%', py: 3, '& .MuiDataGrid-root': { border: 'none' } }}> <Toolbar /> <Container maxWidth={false}> {hasVaultGroups ? <Grid container spacing={3}> <Grid item lg={8} md={12} xl={8} xs={12}>{playerList}</Grid> <Grid item lg={4} md={12} xl={4} xs={12}> <Card> <CardHeader title={lang.vault.permissionGroup} /> <Divider /> <div style={{ height: 594, width: '100%' }}> <DataGrid hideFooter disableColumnMenu rows={groups} columns={columns2} onCellEditCommit={({ field, id, value }) => { let flag = false switch (field) { case 'prefix': flag = true // eslint-disable-next-line no-fallthrough case 'suffix': plugin.emit('vault:setChat', (res: boolean) => { action(res) plugin.emit('vault:fetchGroups', setGroups) }, id, true, flag, value || null) } }} /> </div> </Card> </Grid> </Grid> : playerList} </Container> <PermissionDialog plugin={plugin} id={selectedId} onClose={() => setSelectedId(undefined)} isGroup={isGroup} /> {hasVaultGroups && <Groups plugin={plugin} id={selectedPlayer} onClose={() => setSelectedPlayer(undefined)} groups={groups} />} </Box> }
Example #21
Source File: FireCMSAppBar.tsx From firecms with MIT License | 4 votes |
export function FireCMSAppBar({
title,
handleDrawerToggle,
toolbarExtraWidget
}: CMSAppBarProps) {
const classes = useStyles();
const breadcrumbsContext = useBreadcrumbsContext();
const { breadcrumbs } = breadcrumbsContext;
const authController = useAuthController();
const { mode, toggleMode } = useModeState();
const initial = authController.user?.displayName
? authController.user.displayName[0].toUpperCase()
: (authController.user?.email ? authController.user.email[0].toUpperCase() : "A");
return (
<Slide
direction="down" in={true} mountOnEnter unmountOnExit>
<AppBar
className={classes.appbar}
position={"relative"}
elevation={1}>
<Toolbar>
<IconButton
color="inherit"
aria-label="Open drawer"
edge="start"
onClick={handleDrawerToggle}
className={classes.menuButton}
size="large">
<MenuIcon/>
</IconButton>
<Hidden lgDown>
<Box mr={3}>
<Link
underline={"none"}
key={"breadcrumb-home"}
color="inherit"
component={ReactLink}
to={"/"}>
<Typography variant="h6" noWrap>
{title}
</Typography>
</Link>
</Box>
</Hidden>
<Box mr={2}>
<Breadcrumbs
separator={<NavigateNextIcon
htmlColor={"rgb(0,0,0,0.87)"}
fontSize="small"/>}
aria-label="breadcrumb">
{breadcrumbs.map((entry, index) => (
<Link
underline={"none"}
key={`breadcrumb-${index}`}
color="inherit"
component={ReactLink}
to={entry.url}>
<Chip
classes={{ root: classes.breadcrumb }}
label={entry.title}
/>
</Link>)
)
}
</Breadcrumbs>
</Box>
<Box flexGrow={1}/>
{toolbarExtraWidget &&
<ErrorBoundary>
{
toolbarExtraWidget
}
</ErrorBoundary>}
<Box p={1} mr={1}>
<IconButton
color="inherit"
aria-label="Open drawer"
edge="start"
onClick={() => toggleMode()}
size="large">
{mode === "dark"
? <Brightness3Icon/>
: <Brightness5Icon/>}
</IconButton>
</Box>
<Box p={1} mr={1}>
{authController.user && authController.user.photoURL
? <Avatar
src={authController.user.photoURL}/>
: <Avatar>{initial}</Avatar>
}
</Box>
<Button variant="text"
color="inherit"
onClick={authController.signOut}>
Log Out
</Button>
</Toolbar>
</AppBar>
</Slide>
);
}
Example #22
Source File: Config.tsx From NekoMaid with MIT License | 4 votes |
configs.push({
title: lang.config.serverConfig,
component () {
const plugin = usePlugin()
const globalData = useGlobalData()
const [flag, update] = useState(0)
const [info, setInfo] = useState<Record<string, string>>({ })
const [open, setOpen] = useState(false)
const [canGetData, setCanGetData] = useState(true)
const [loading, setLoading] = useState(false)
const setValue = (field: string, value: any, isGlobal = true) => {
plugin.emit('server:set', field, value)
success()
if (isGlobal) {
(globalData as any)[field] = value
update(flag + 1)
location.reload()
}
}
const createEditButtom = (field: string, isGlobal?: boolean, isInt = true) => <IconButton
onClick={() => dialog(
{
content: lang.inputValue,
input: isInt
? {
error: true,
type: 'number',
helperText: lang.invalidValue,
validator: (it: string) => /^\d+$/.test(it) && +it >= 0
}
: { }
}).then(res => res != null && setValue(field, isInt ? parseInt(res as any) : (res || null), isGlobal))}
><Edit /></IconButton>
const infoElm: JSX.Element[] = []
for (const key in info) {
const name = (lang.config as any)[key]
infoElm.push(<ListItem key={key} sx={{ pl: 4 }}>
<ListItemText
primary={key === 'isAikarFlags' ? <Link href='https://mcflags.emc.gs' target='_blank' rel='noopener'>{name}</Link> : name}
secondary={info[key].toString()}
/>
</ListItem>)
}
return <List>
<CircularLoading loading={loading} />
<ListItem secondaryAction={globalData.canSetMaxPlayers
? createEditButtom('maxPlayers')
: undefined}>
<ListItemText primary={lang.config.maxPlayers + ': ' + globalData.maxPlayers} />
</ListItem>
<ListItem secondaryAction={createEditButtom('spawnRadius')}>
<ListItemText primary={lang.config.spawnRadius + ': ' + globalData.spawnRadius} />
</ListItem>
<ListItem secondaryAction={createEditButtom('motd', false, false)}>
<ListItemText primary={lang.config.motd} />
</ListItem>
<ListItem secondaryAction={<Switch checked={globalData.hasWhitelist} onChange={e => setValue('hasWhitelist', e.target.checked)} />}>
<ListItemText primary={lang.config.whitelist} />
</ListItem>
{canGetData && <>
<ListItemButton onClick={() => {
if (infoElm.length) setOpen(!open)
else {
setLoading(true)
plugin.emit('server:fetchInfo', (data: any) => {
setLoading(false)
if (!data) {
failed(lang.unsupported)
setCanGetData(false)
return
}
setInfo(data)
setOpen(true)
})
}
}}>
<ListItemIcon><Equalizer /></ListItemIcon>
<ListItemText primary={lang.info} />
{open ? <ExpandLess /> : <ExpandMore />}
</ListItemButton>
<Collapse in={open} timeout='auto' unmountOnExit>
<List component='div' dense disablePadding>{infoElm}</List>
</Collapse>
</>}
</List>
}
},
{
title: lang.history,
component () {
const [cur, update] = useState(0)
const list: ServerRecord[] = JSON.parse(localStorage.getItem('NekoMaid:servers') || '[]')
return <List>
{list.sort((a, b) => b.time - a.time).map(it => {
const i = it.address.indexOf('?')
return <ListItem
disablePadding
key={it.address}
secondaryAction={<IconButton edge='end' size='small' onClick={() => {
localStorage.setItem('NekoMaid:servers', JSON.stringify(list.filter(s => s.address !== it.address)))
success()
update(cur + 1)
}}><Delete /></IconButton>}
>
<ListItemButton onClick={() => {
location.hash = ''
location.search = it.address
}} dense>
<ListItemAvatar><Avatar src={it.icon} variant='rounded'><HelpOutline /></Avatar></ListItemAvatar>
<ListItemText primary={<Tooltip title={it.address.slice(i + 1)}>
<span>{it.address.slice(0, i)}</span></Tooltip>} secondary={dayjs(it.time).fromNow()} />
</ListItemButton>
</ListItem>
})}
</List>
}
},
{
title: lang.config.theme,
component () {
const color = localStorage.getItem('NekoMaid:color') || 'blue'
return <CardContent sx={{ textAlign: 'center' }}>
<Box>
<ToggleButtonGroup exclusive value={localStorage.getItem('NekoMaid:colorMode') || ''} onChange={(_, it) => {
localStorage.setItem('NekoMaid:colorMode', it)
location.reload()
}}>
<ToggleButton value='light'><Brightness7 /> {lang.config.light}</ToggleButton>
<ToggleButton value=''><SettingsBrightness /> {lang.config.system}</ToggleButton>
<ToggleButton value='dark'><Brightness4 /> {lang.config.dark}</ToggleButton>
</ToggleButtonGroup>
</Box>
<Paper sx={{ marginTop: 2, width: '176px', overflow: 'hidden', display: 'inline-block' }}>
{Object.keys(colors).slice(1, 17).map((key, i) => {
const checked = color === key
const elm = <Box
key={key}
onClick={() => {
localStorage.setItem('NekoMaid:color', key)
location.reload()
}}
sx={{
backgroundColor: (colors as any)[key][600],
width: '44px',
height: '44px',
display: 'inline-block',
cursor: 'pointer'
}}
><Check htmlColor='white' sx={{ top: '10px', position: 'relative', opacity: checked ? 1 : 0 }} /></Box>
return (i + 1) % 4 === 0 ? <React.Fragment key={key}>{elm}<br /></React.Fragment> : elm
})}
</Paper>
</CardContent>
}
})
Example #23
Source File: CommentItemModule.tsx From bouncecode-cms with GNU General Public License v3.0 | 4 votes |
function CommentItemModule({post, comment}: ICommentItemModule) {
const [moreVert, setMoreVert] = React.useState(null);
const openMoreVert = Boolean(moreVert);
const toggleMoreVert = open => {
return event => {
if (open) {
setMoreVert(event.currentTarget);
} else {
setMoreVert(null);
}
};
};
const [commentDrawer, setCommentDrawer] = React.useState(null);
const toggleCommentDrawer = open => {
return event => {
if (
event.type === 'keydown' &&
(event.key === 'Tab' || event.key === 'Shift')
) {
return;
}
setCommentDrawer(open);
};
};
const [
emotionMutation,
{loading: emotionLoading},
] = useCommentEmotionMutation();
const [
undoEmotionMutation,
{loading: undoEmotionLoading},
] = useCommentEmotionUndoMutation();
const updateEmotion = emotion => {
return () => {
console.log(myEmotion);
console.log(emotion);
if (myEmotion === emotion.toLowerCase()) {
undoEmotionMutation({
variables: {
where: {
id: comment.id,
},
},
refetchQueries: [
{
query: CommentMyEmotionDocument,
variables: {where: {id: comment.id}},
},
],
});
} else {
emotionMutation({
variables: {
where: {
id: comment.id,
},
data: {emotion},
},
refetchQueries: [
{
query: CommentMyEmotionDocument,
variables: {where: {id: comment.id}},
},
],
});
}
};
};
const {
data: myEmotionData,
loading: myEmotionLoading,
} = useCommentMyEmotionQuery({
variables: {where: {id: comment.id}},
});
const myEmotion = myEmotionData?.commentMyEmotion?.emotion;
return (
<Grid container direction="column" spacing={1}>
<Grid item>
<Grid container spacing={2}>
<Grid item>
<Avatar></Avatar>
</Grid>
<Grid item xs>
<Grid container direction="column" spacing={1}>
<Grid item>
<Grid container spacing={2} alignItems="center">
<Grid item>
<Typography variant="body2">
{comment.user.email}
</Typography>
</Grid>
<Grid item>
<Typography variant="caption">
{formatDistance(
new Date(comment.createdDate),
new Date(),
{addSuffix: true},
)}
</Typography>
</Grid>
</Grid>
</Grid>
<Grid item>
<Typography variant="body1">{comment.text}</Typography>
</Grid>
<Grid
container
justifyContent="space-between"
alignItems="center">
<Grid item>
<Grid container alignItems="center">
<Grid item>
<Button
variant="text"
size="small"
startIcon={<ThumbUpOutlined />}
onClick={updateEmotion('LIKE')}
disabled={
myEmotionLoading ||
emotionLoading ||
undoEmotionLoading
}>
{comment.like || ''}
</Button>
</Grid>
<Grid item>
<Button
variant="text"
size="small"
startIcon={<ThumbDownOutlined />}
onClick={updateEmotion('UNLIKE')}
disabled={
myEmotionLoading ||
emotionLoading ||
undoEmotionLoading
}>
{comment.unlike || ''}
</Button>
</Grid>
{post ? (
<Grid item>
<Button
variant="text"
size="small"
onClick={toggleCommentDrawer(true)}
disabled>
답글
</Button>
<Drawer
anchor="bottom"
open={commentDrawer}
onClose={toggleCommentDrawer(false)}>
<Container maxWidth="md">
<Box pt={2} pb={2}>
<CommentCreateFormModule />
</Box>
</Container>
</Drawer>
</Grid>
) : (
undefined
)}
</Grid>
</Grid>
<Grid item>
<IconButton onClick={toggleMoreVert(true)} disabled>
<MoreVert fontSize="small" />
</IconButton>
<Menu
anchorEl={moreVert}
open={openMoreVert}
onClose={toggleMoreVert(false)}>
<MenuItem
// selected={option === 'Pyxis'}
onClick={toggleMoreVert(false)}>
<ListItemIcon>
<AssistantPhotoOutlined />
</ListItemIcon>
<Typography variant="inherit">신고</Typography>
</MenuItem>
</Menu>
</Grid>
</Grid>
{/* {post ? (
<Grid item>
<Grid container direction="column" spacing={2}>
<Grid item>
<Button
variant="text"
size="small"
startIcon={<KeyboardArrowDown />}>
답글 1개 보기
</Button>
</Grid>
<Grid item>
<CommentItemModule comment />
</Grid>
</Grid>
</Grid>
) : (
undefined
)} */}
</Grid>
</Grid>
</Grid>
</Grid>
</Grid>
);
}
Example #24
Source File: CommentCreateFormModule.tsx From bouncecode-cms with GNU General Public License v3.0 | 4 votes |
function CommentCreateFormModule({post}: ICommentDefaultListModule) {
const postId = post?.id;
const {
values,
handleSubmit,
handleChange,
errors,
touched,
isSubmitting,
handleReset,
} = useCommentCreateFormik(postId);
console.log('errors', errors);
const {data} = useMeQuery();
return (
<form onSubmit={handleSubmit}>
<Grid container spacing={2}>
<Grid item>
<Avatar></Avatar>
</Grid>
<Grid item xs>
<Grid container spacing={2} direction="column">
<Grid item>
<TextField
name="text"
// label="댓글을 입력해주세요."
placeholder="댓글을 입력해주세요."
multiline
// variant="outlined"
value={values.text}
onChange={handleChange}
error={touched.text && Boolean(errors.text)}
helperText={touched.text ? errors.text : ''}
fullWidth
disabled={isSubmitting}
/>
</Grid>
<Grid item>
<Grid
container
justifyContent="space-between"
alignItems="center">
<Grid item>
{/* <Rating
name="rating"
onChange={handleChange}
value={values.rating}
/> */}
</Grid>
<Grid item>
<Grid container spacing={1} alignItems="center">
<Grid item>
<Button
fullWidth
variant="text"
color="primary"
onClick={handleReset}>
취소
</Button>
</Grid>
<Grid item>
<Button
fullWidth
type="submit"
variant="contained"
color="primary"
// size="large"
disabled={!values.text || isSubmitting}
endIcon={
isSubmitting ? (
<CircularProgress size={16} />
) : (
undefined
)
}>
댓글
</Button>
</Grid>
</Grid>
</Grid>
</Grid>
</Grid>
</Grid>
</Grid>
</Grid>
</form>
);
}
Example #25
Source File: Message.tsx From airmessage-web with Apache License 2.0 | 4 votes |
export default function Message(props: Props) {
//State
const [attachmentDataArray, setAttachmentDataArray] = useState<FileDownloadResult[]>([]);
const [dialogOpen, setDialogOpen] = useState<"error" | "rawError" | undefined>(undefined);
function closeDialog() {
setDialogOpen(undefined);
}
function openDialogError() {
setDialogOpen("error");
}
function openDialogRawError() {
setDialogOpen("rawError");
}
function copyRawErrorAndClose() {
navigator.clipboard.writeText(props.message.error!.detail!);
closeDialog();
}
//Getting the message information
const isOutgoing = props.message.sender === undefined;
const displayAvatar = !isOutgoing && !props.flow.anchorTop;
const displaySender = props.isGroupChat && displayAvatar;
const messageConfirmed = props.message.status !== MessageStatusCode.Unconfirmed;
//Building the message style
const theme = useTheme();
let colorPalette: PaletteColor;
if(isOutgoing) {
if(props.service === appleServiceAppleMessage) colorPalette = theme.palette.messageOutgoing;
else colorPalette = theme.palette.messageOutgoingTextMessage;
} else {
colorPalette = theme.palette.messageIncoming;
}
const messagePartPropsBase: Partial<MessagePartProps> = {
alignSelf: isOutgoing ? "flex-end" : "flex-start",
color: colorPalette.contrastText,
backgroundColor: colorPalette.main,
opacity: messageConfirmed ? opacityConfirmed : opacityUnconfirmed
};
//Splitting the modifiers for each message part
const stickerGroups = props.message.stickers.reduce((accumulator: {[index: number]: StickerItem[]}, item: StickerItem) => {
if(accumulator[item.messageIndex]) accumulator[item.messageIndex].push(item);
else accumulator[item.messageIndex] = [item];
return accumulator;
}, {});
const tapbackGroups = props.message.tapbacks.reduce((accumulator: {[index: number]: TapbackItem[]}, item: TapbackItem) => {
if(accumulator[item.messageIndex]) accumulator[item.messageIndex].push(item);
else accumulator[item.messageIndex] = [item];
return accumulator;
}, {});
//Adding the message text
const components: React.ReactNode[] = [];
if(props.message.text) {
const partProps: MessagePartProps = {
...messagePartPropsBase,
borderRadius: getBorderRadius(props.flow.anchorTop, props.flow.anchorBottom || props.message.attachments.length > 0, isOutgoing),
marginTop: 0
} as MessagePartProps;
const component = <MessageBubble key="messagetext" text={props.message.text!} index={0} partProps={partProps} stickers={stickerGroups[0]} tapbacks={tapbackGroups[0]} />;
components.push(component);
}
function onAttachmentData(attachmentIndex: number, shouldDownload: boolean, result: FileDownloadResult) {
if(shouldDownload) {
//Downloading the file
const attachment = props.message.attachments[attachmentIndex];
downloadArrayBuffer(result.data, result.downloadType ?? attachment.type, result.downloadName ?? attachment.name);
} else {
//Updating the data
const newArray = [...attachmentDataArray];
newArray[attachmentIndex] = result;
setAttachmentDataArray(newArray);
}
}
function downloadData(attachmentIndex: number, data: ArrayBuffer | Blob) {
const attachment = props.message.attachments[attachmentIndex];
if(data instanceof ArrayBuffer) {
downloadArrayBuffer(data, attachment.type, attachment.name);
} else {
downloadBlob(data, attachment.type, attachment.name);
}
}
/**
* Computes the file data to display to the user
*/
const getComputedFileData = useCallback((attachmentIndex: number): FileDisplayResult => {
const attachment = props.message.attachments[attachmentIndex];
const downloadData = attachmentDataArray[attachmentIndex];
if(downloadData === undefined) {
return {
data: attachment.data,
name: attachment.name,
type: attachment.type
};
} else return {
data: downloadData.data,
name: downloadData.downloadName ?? attachment.name,
type: downloadData.downloadType ?? attachment.type
};
}, [props.message.attachments, attachmentDataArray]);
//Adding the attachments
for(let i = 0; i < props.message.attachments.length; i++) {
const index = props.message.text ? i + 1 : i;
const attachment = props.message.attachments[i];
const partProps: MessagePartProps = {
...messagePartPropsBase,
borderRadius: getBorderRadius(props.flow.anchorTop || index > 0, props.flow.anchorBottom || i + 1 < props.message.attachments.length, isOutgoing),
marginTop: index > 0 ? marginLinked : undefined
} as MessagePartProps;
//Checking if the attachment has data
const attachmentData = getComputedFileData(i);
if(attachmentData.data !== undefined && isAttachmentPreviewable(attachmentData.type)) {
//Custom background color
const imagePartProps = {
...partProps,
backgroundColor: theme.palette.background.sidebar,
};
if(attachmentData.type.startsWith("image/")) {
components.push(<MessageAttachmentImage key={attachment.guid ?? attachment.localID} data={attachmentData.data} name={attachmentData.name} type={attachmentData.type} partProps={imagePartProps} stickers={stickerGroups[index]} tapbacks={tapbackGroups[index]} />);
}
} else {
//Adding a generic download attachment
components.push(<MessageAttachmentDownloadable
key={attachment.guid ?? attachment.localID}
data={attachmentData.data}
name={attachmentData.name}
type={attachmentData.type}
size={attachment.size}
guid={attachment.guid!}
onDataAvailable={(data) => onAttachmentData(i, !isAttachmentPreviewable(data.downloadType ?? attachment.type), data)}
onDataClicked={(data) => downloadData(i, data)}
partProps={partProps}
stickers={stickerGroups[index]}
tapbacks={tapbackGroups[index]} />);
}
}
const messageStyle: CSSProperties = {
marginTop: props.flow.anchorTop ? marginLinked : marginUnlinked,
};
//Initializing state
const [personData, setPersonData] = useState<PersonData | undefined>();
useEffect(() => {
if(!props.message.sender) return;
//Requesting contact data
findPerson(props.message.sender).then(setPersonData, console.warn);
}, [props.message.sender]);
//Building and returning the component
return (
<div className={styles.message} style={messageStyle}>
{props.flow.showDivider && <Typography className={styles.separator} variant="body2" color="textSecondary">{getTimeDivider(props.message.date)}</Typography>}
{displaySender && <Typography className={styles.labelSender} variant="caption" color="textSecondary">{personData?.name ?? props.message.sender}</Typography>}
<div className={styles.messageSplit}>
{<Avatar className={styles.avatar} src={personData?.avatar} style={displayAvatar ? {visibility: "visible", backgroundColor: colorFromContact(props.message.sender ?? "")} : {visibility: "hidden"}} />}
<div className={styles.messageParts}>
{components}
</div>
{props.message.progress && !props.message.error && <CircularProgress className={styles.messageProgress} size={24} variant={props.message.progress === -1 ? "indeterminate" : "determinate"} value={props.message.progress} />}
{props.message.error && <IconButton className={styles.messageError} style={{color: theme.palette.error.light}} size="small" onClick={openDialogError}>
<ErrorRounded />
</IconButton>}
<Dialog open={dialogOpen === "error"} onClose={closeDialog}>
<DialogTitle>Your message could not be sent</DialogTitle>
{props.message.error !== undefined && <React.Fragment>
<DialogContent>
<DialogContentText>{messageErrorToUserString(props.message.error!.code)}</DialogContentText>
</DialogContent>
<DialogActions>
{props.message.error!.detail && <Button onClick={openDialogRawError} color="primary">
Error details
</Button>}
<Button onClick={closeDialog} color="primary" autoFocus>
Dismiss
</Button>
</DialogActions>
</React.Fragment>}
</Dialog>
<Dialog open={dialogOpen === "rawError"} onClose={closeDialog}>
<DialogTitle>Error details</DialogTitle>
{props.message.error !== undefined && <React.Fragment>
<DialogContent>
<DialogContentText className={styles.rawErrorText}>{props.message.error.detail!}</DialogContentText>
</DialogContent>
<DialogActions>
<Button onClick={copyRawErrorAndClose} color="primary">
Copy to clipboard
</Button>
<Button onClick={closeDialog} color="primary" autoFocus>
Dismiss
</Button>
</DialogActions>
</React.Fragment>}
</Dialog>
</div>
{props.showStatus && <Typography className={styles.labelStatus} variant="caption" color="textSecondary">{getStatusString(props.message)}</Typography>}
</div>
);
}
Example #26
Source File: DetailCreate.tsx From airmessage-web with Apache License 2.0 | 4 votes |
export default function DetailCreate(props: {onConversationCreated: (conversation: Conversation) => void}) { const [query, setQuery] = useState<string>(""); const [peopleSelection, setPeopleSelection] = useState<SelectionData[]>([]); const [peoplePool, setPeoplePool] = useState<PersonData[]>(); const [isLoading, setLoading] = useState(false); const displaySnackbar = useContext(SnackbarContext); const theme = useTheme(); useEffect(() => { //Loading the people getPeople().then((people) => { setPeoplePool(people); }); }, []); const groupedPeople = peoplePool ? filterAndGroup(peoplePool, query) : undefined; function handleTextChange(event: React.ChangeEvent<HTMLInputElement>) { const text = event.target.value; //Updating the query text setQuery(text); } function handleKeyDown(event: React.KeyboardEvent<any>) { if(event.key === "Backspace") { //Removing the last person if(query.length === 0 && peopleSelection.length > 0) { setPeopleSelection(peopleSelection.slice(0, peopleSelection.length - 1)); event.preventDefault(); } } else if(event.key === "Enter") { //Manually entering the query handleDirectEntry(); } } function handleAddressClick(person: PersonData, address: AddressData) { //Clearing the query text setQuery(""); //Toggling the item const index = peopleSelection.findIndex((selection) => selection.address === address.value); if(index === -1) { setPeopleSelection(peopleSelection.concat({ name: person.name, avatar: person.avatar, address: address.value, displayAddress: address.displayValue, addressLabel: address.label })); } else { setPeopleSelection([...peopleSelection.slice(0, index), ...peopleSelection.slice(index + 1)]); } } function findPersonInfo(address: string): [PersonData, AddressData] | undefined { if(!peoplePool) return undefined; for(const person of peoplePool) { for(const personAddress of person.addresses) { if(personAddress.value === address) { return [person, personAddress]; } } } return undefined; } function handleDirectEntry() { const address = query; //Checking if the item is an email address if(address.match(regexEmail)) { //Clearing the query text setQuery(""); //Returning if the addition will conflict with any existing entries if(peopleSelection.find((selection) => selection.address === address)) return; //Searching for the user in the listings const [personData, addressData] = findPersonInfo(query) ?? [undefined, undefined]; //Adding the item setPeopleSelection(peopleSelection.concat({ name: personData?.name, avatar: personData?.avatar, address: address, displayAddress: address, addressLabel: addressData?.label })); } else { //Checking if the item is a phone number const phone = parsePhoneNumberFromString(query, "US"); const formatted = phone?.number.toString(); if(phone && phone.isValid()) { //Clearing the query text setQuery(""); //Returning if the addition will conflict with any existing entries if(peopleSelection.find((selection) => selection.address === formatted!)) return; //Searching for the user in the listings const [personData, addressData] = findPersonInfo(formatted!) ?? [undefined, undefined]; //Adding the item setPeopleSelection(peopleSelection.concat({ name: personData?.name, avatar: personData?.avatar, address: formatted!, displayAddress: phone.formatNational(), addressLabel: addressData?.label })); } } } function handleRemoveSelection(selection: SelectionData) { setPeopleSelection(peopleSelection.filter((value) => value !== selection)); } function confirmParticipants() { //Starting the loading view setLoading(true); //Mapping the people selection to their addresses const chatMembers = peopleSelection.map((selection) => selection.address); ConnectionManager.createChat(chatMembers, messagingService) .then((chatGUID) => { //Adding the chat props.onConversationCreated({ localID: generateConversationLocalID(), guid: chatGUID, service: messagingService, members: chatMembers, preview: { type: ConversationPreviewType.ChatCreation, date: new Date() }, localOnly: false, unreadMessages: false }); }).catch(([errorCode, errorDesc]: [CreateChatErrorCode, string | undefined]) => { if(errorCode === CreateChatErrorCode.NotSupported) { //If the server doesn't support creating chats, //create this chat locally and defer its creation //until the user sends a message props.onConversationCreated({ localID: generateConversationLocalID(), service: messagingService, members: chatMembers, preview: { type: ConversationPreviewType.ChatCreation, date: new Date() }, localOnly: true, unreadMessages: false }); return; } //Cancelling loading setLoading(false); //Displaying a snackbar displaySnackbar({message: "Failed to create conversation"}); //Logging the error console.warn(`Failed to create chat: ${errorCode} / ${errorDesc}`); }); } let queryDirectState: boolean; //Is the current input query a valid address that can be added directly? let queryDirectDisplay: string; { //Checking email address if(query.match(regexEmail)) { queryDirectState = true; queryDirectDisplay = query; } else { //Checking phone number const phone = parsePhoneNumberFromString(query, "US"); if(phone && phone.isValid()) { queryDirectState = true; queryDirectDisplay = phone.formatNational(); } else { queryDirectState = false; queryDirectDisplay = query; } } } return ( <DetailFrame title="New conversation"> <div className={styles.content}> <div className={`${isLoading ? styles.scrimShown : styles.scrimHidden} ${styles.progressContainer}`} style={{backgroundColor: theme.palette.mode === "light" ? "rgba(255, 255, 255, 0.7)" : "rgba(0, 0, 0, 0.7)"}}> <CircularProgress /> </div> <div className={styles.participantInputWrapper}> <div className={styles.participantInputLayout} style={{backgroundColor: theme.palette.messageIncoming.main}}> <div className={styles.participantInputFlow}> <SelectionList className={styles.participantInputChip} selections={peopleSelection} onRemove={handleRemoveSelection} /> <InputBase className={styles.participantInputField} placeholder={peopleSelection.length > 0 ? undefined : "Type a name, email address, or phone number"} value={query} onChange={handleTextChange} onKeyDown={handleKeyDown} autoFocus /> </div> <Button className={styles.participantInputButton} variant="contained" color="primary" disabled={peopleSelection.length === 0} disableElevation onClick={confirmParticipants}>Done</Button> </div> </div> { groupedPeople ? <div className={styles.listWrapper}> <div className={styles.list}> {queryDirectState && <DirectSendButton address={queryDirectDisplay} onClick={handleDirectEntry} />} {Object.entries(groupedPeople).map(([firstLetter, people]) => ( <React.Fragment key={`section-${firstLetter}`}> <ListSubheader>{firstLetter}</ListSubheader> {people.map((person) => ( <div key={person.id} className={styles.contactRoot}> <Avatar src={person.avatar} alt={person.name} /> <div className={styles.contactText}> <Typography variant="body1" color="textPrimary">{person.name}</Typography> <div className={styles.contactAddresses}> {person.addresses.map((address) => ( <AddressButton key={address.value + "/" + address.label} address={address} selected={peopleSelection.find((selection) => selection.address === address.value) !== undefined} onClick={() => handleAddressClick(person, address)} /> ))} </div> </div> </div> ))} </React.Fragment> ))} </div> </div> : <div className={`${styles.listWrapper} ${styles.progressContainer}`}> <CircularProgress /> </div> } </div> </DetailFrame> ); }
Example #27
Source File: MangaCard.tsx From Tachidesk-WebUI with Mozilla Public License 2.0 | 4 votes |
MangaCard = React.forwardRef<HTMLDivElement, IProps>((props: IProps, ref) => {
const {
manga: {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
id, title, thumbnailUrl, downloadCount, unreadCount: unread, inLibrary,
},
gridLayout,
dimensions,
} = props;
const { options: { showUnreadBadge, showDownloadBadge } } = useLibraryOptionsContext();
const [serverAddress] = useLocalStorage<String>('serverBaseURL', '');
const [useCache] = useLocalStorage<boolean>('useCache', true);
const [ItemWidth] = useLocalStorage<number>('ItemWidth', 300);
if (gridLayout !== 2) {
const colomns = Math.round(dimensions / ItemWidth);
return (
// @ts-ignore gridsize type isnt allowed to be a decimal but it works fine
<Grid item xs={12 / colomns} sm={12 / colomns} md={12 / colomns} lg={12 / colomns}>
<Link to={`/manga/${id}/`} style={(gridLayout === 1) ? { textDecoration: 'none' } : {}}>
<Box
sx={{
display: 'flex',
flexDirection: 'column',
}}
>
<Card
sx={{
// force standard aspect ratio of manga covers
aspectRatio: '225/350',
display: 'flex',
}}
ref={ref}
>
<CardActionArea
sx={{
position: 'relative',
height: '100%',
}}
>
<BadgeContainer>
{inLibrary && (
<Typography
sx={{ backgroundColor: 'primary.dark', zIndex: '1' }}
>
In library
</Typography>
)}
{ showUnreadBadge && unread! > 0 && (
<Typography
sx={{ backgroundColor: 'primary.dark' }}
>
{unread}
</Typography>
)}
{ showDownloadBadge && downloadCount! > 0 && (
<Typography sx={{
backgroundColor: 'success.dark',
}}
>
{downloadCount}
</Typography>
)}
</BadgeContainer>
<SpinnerImage
alt={title}
src={`${serverAddress}${thumbnailUrl}?useCache=${useCache}`}
imgStyle={inLibrary
? {
height: '100%',
width: '100%',
objectFit: 'cover',
filter: 'brightness(0.4)',
}
: {
height: '100%',
width: '100%',
objectFit: 'cover',
}}
spinnerStyle={{
display: 'grid',
placeItems: 'center',
}}
/>
{(gridLayout === 1) ? (<></>) : (
<>
<BottomGradient />
<BottomGradientDoubledDown />
</>
)}
{(gridLayout === 1) ? (
<></>
) : (
<MangaTitle>
{truncateText(title, 61)}
</MangaTitle>
)}
</CardActionArea>
</Card>
{(gridLayout === 1) ? (
<MangaTitle
sx={{
position: 'relative',
}}
>
{truncateText(title, 61)}
</MangaTitle>
) : (<></>)}
</Box>
</Link>
</Grid>
);
}
return (
<Grid item xs={12} sm={12} md={12} lg={12}>
<Link to={`/manga/${id}/`} style={{ textDecoration: 'none', color: 'unset' }}>
<CardContent sx={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
padding: 2,
'&:hover': {
backgroundColor: 'action.hover',
transition: 'background-color 100ms cubic-bezier(0.4, 0, 0.2, 1) 0ms',
},
'&:active': {
backgroundColor: 'action.selected',
transition: 'background-color 100ms cubic-bezier(0.4, 0, 0.2, 1) 0ms',
},
position: 'relative',
}}
>
<Avatar
variant="rounded"
sx={inLibrary
? {
width: 56,
height: 56,
flex: '0 0 auto',
marginRight: 2,
imageRendering: 'pixelated',
filter: 'brightness(0.4)',
}
: {
width: 56,
height: 56,
flex: '0 0 auto',
marginRight: 2,
imageRendering: 'pixelated',
}}
src={`${serverAddress}${thumbnailUrl}?useCache=${useCache}`}
/>
<Box
sx={{
display: 'flex',
flexDirection: 'row',
flexGrow: 1,
width: 'min-content',
}}
>
<Typography variant="h5" component="h2">
{truncateText(title, 61)}
</Typography>
</Box>
<BadgeContainer sx={{ position: 'relative' }}>
{inLibrary && (
<Typography
sx={{ backgroundColor: 'primary.dark', zIndex: '1' }}
>
In library
</Typography>
)}
{ showUnreadBadge && unread! > 0 && (
<Typography
sx={{ backgroundColor: 'primary.dark' }}
>
{unread}
</Typography>
)}
{ showDownloadBadge && downloadCount! > 0 && (
<Typography sx={{
backgroundColor: 'success.dark',
}}
>
{downloadCount}
</Typography>
)}
</BadgeContainer>
</CardContent>
</Link>
</Grid>
);
})
Example #28
Source File: Register.tsx From abrechnung with GNU Affero General Public License v3.0 | 4 votes |
export default function Register() {
const loggedIn = useRecoilValue(isAuthenticated);
const [loading, setLoading] = useState(true);
const query = useQuery();
const history = useHistory();
const queryArgsForward = query.get("next") != null ? "?next=" + query.get("next") : "";
useTitle("Abrechnung - Register");
useEffect(() => {
if (loggedIn) {
setLoading(false);
if (query.get("next") !== null && query.get("next") !== undefined) {
history.push(query.get("next"));
} else {
history.push("/");
}
} else {
setLoading(false);
}
}, [loggedIn, history, query]);
const handleSubmit = (values, { setSubmitting }) => {
// extract a potential invite token (which should be a uuid) from the query args
let inviteToken = undefined;
console.log(query.get("next"));
if (query.get("next") !== null && query.get("next") !== undefined) {
const re = /\/invite\/([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})/;
const m = query.get("next").match(re);
if (m != null) {
inviteToken = m[1];
}
}
const payload = {
...values,
inviteToken: inviteToken,
};
register(payload)
.then((res) => {
toast.success(`Registered successfully, please confirm your email before logging in...`, {
autoClose: 20000,
});
setSubmitting(false);
history.push(`/login${queryArgsForward}`);
})
.catch((err) => {
toast.error(err);
setSubmitting(false);
});
};
const validate = (values) => {
let errors = {};
if (values.password !== values.password2) {
errors["password2"] = "Passwords do not match";
}
return errors;
};
return (
<>
{loading ? (
<Loading />
) : (
<>
<Container maxWidth="xs">
<CssBaseline />
<Box sx={{ display: "flex", flexDirection: "column", alignItems: "center" }}>
<Avatar sx={{ margin: 1, backgroundColor: "primary.main" }}>
<LockOutlined />
</Avatar>
<Typography component="h1" variant="h5">
Register a new account
</Typography>
<Formik
validate={validate}
validationSchema={validationSchema}
initialValues={{
username: "",
email: "",
password: "",
password2: "",
}}
onSubmit={handleSubmit}
>
{({ values, handleBlur, handleChange, handleSubmit, isSubmitting }) => (
<Form onSubmit={handleSubmit}>
<TextField
variant="outlined"
margin="normal"
required
fullWidth
autoFocus
type="text"
label="Username"
name="username"
onBlur={handleBlur}
onChange={handleChange}
value={values.username}
/>
<TextField
variant="outlined"
margin="normal"
required
fullWidth
type="email"
name="email"
label="E-Mail"
onBlur={handleBlur}
onChange={handleChange}
value={values.email}
/>
<TextField
variant="outlined"
margin="normal"
required
fullWidth
type="password"
name="password"
label="Password"
onBlur={handleBlur}
onChange={handleChange}
value={values.password}
/>
<TextField
variant="outlined"
margin="normal"
required
fullWidth
type="password"
name="password2"
label="Repeat Password"
onBlur={handleBlur}
onChange={handleChange}
value={values.password2}
/>
{isSubmitting && <LinearProgress />}
<Button
type="submit"
fullWidth
variant="contained"
color="primary"
disabled={isSubmitting}
sx={{ mt: 1 }}
>
Register
</Button>
<Grid container={true} sx={{ justifyContent: "flex-end" }}>
<Grid item>
<Link
to={`/login${queryArgsForward}`}
component={RouterLink}
variant="body2"
>
Already have an account? Sign in
</Link>
</Grid>
</Grid>
</Form>
)}
</Formik>
</Box>
</Container>
</>
)}
</>
);
}
Example #29
Source File: Login.tsx From abrechnung with GNU Affero General Public License v3.0 | 4 votes |
export default function Login() {
const setUserData = useSetRecoilState(userData);
const isLoggedIn = useRecoilValue(isAuthenticated);
const query = useQuery();
const history = useHistory();
const queryArgsForward = query.get("next") != null ? "?next=" + query.get("next") : "";
useTitle("Abrechnung - Login");
useEffect(() => {
if (isLoggedIn) {
if (query.get("next") !== null && query.get("next") !== undefined) {
history.push(query.get("next"));
} else {
history.push("/");
}
}
}, [isLoggedIn, history, query]);
const handleSubmit = (values, { setSubmitting }) => {
login(values)
.then((res) => {
toast.success(`Logged in...`);
setSubmitting(false);
fetchProfile()
.then((result) => {
setUserData(result);
})
.catch((err) => {
toast.error(err);
removeToken();
setUserData(null);
});
})
.catch((err) => {
toast.error(err);
setSubmitting(false);
removeToken();
setUserData(null);
});
};
return (
<Container component="main" maxWidth="xs">
<CssBaseline />
<Box sx={{ display: "flex", flexDirection: "column", alignItems: "center" }}>
<Avatar sx={{ margin: 1, backgroundColor: "primary.main" }}>
<LockOutlined />
</Avatar>
<Typography component="h1" variant="h5">
Sign in
</Typography>
<Formik
initialValues={{ password: "", username: "" }}
onSubmit={handleSubmit}
validationSchema={validationSchema}
>
{({ values, handleBlur, handleChange, handleSubmit, isSubmitting }) => (
<Form onSubmit={handleSubmit}>
<input type="hidden" name="remember" value="true" />
<TextField
variant="outlined"
margin="normal"
required
fullWidth
autoFocus
type="text"
label="Username"
name="username"
onBlur={handleBlur}
onChange={handleChange}
value={values.username}
/>
<TextField
variant="outlined"
margin="normal"
required
fullWidth
type="password"
name="password"
label="Password"
onBlur={handleBlur}
onChange={handleChange}
value={values.password}
/>
{isSubmitting && <LinearProgress />}
<Button
type="submit"
fullWidth
variant="contained"
color="primary"
disabled={isSubmitting}
sx={{ mt: 1 }}
>
Login
</Button>
<Grid container={true} sx={{ justifyContent: "flex-end" }}>
<Grid item>
<Link to={`/register${queryArgsForward}`} component={RouterLink} variant="body2">
No account? register
</Link>
</Grid>
</Grid>
<Grid container={true} sx={{ justifyContent: "flex-end" }}>
<Grid item>
<Link to="/recover-password" component={RouterLink} variant="body2">
Forgot your password?
</Link>
</Grid>
</Grid>
</Form>
)}
</Formik>
</Box>
</Container>
);
}