@mui/icons-material#Add TypeScript Examples
The following examples show how to use
@mui/icons-material#Add.
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: AddToQueue.tsx From multi-downloader-nx with MIT License | 6 votes |
AddToQueue: React.FC = () => {
const [isOpen, setOpen] = React.useState(false);
return <Box>
<EpisodeListing />
<Dialog open={isOpen} onClose={() => setOpen(false)} maxWidth='md'>
<Box sx={{ border: '2px solid white', p: 2 }}>
<SearchBox />
<Divider variant='middle' className="divider-width" light sx={{ color: 'text.primary', fontSize: '1.2rem' }}>Options</Divider>
<DownloadSelector onFinish={() => setOpen(false)} />
</Box>
</Dialog>
<Button variant='contained' onClick={() => setOpen(true)}>
<Add />
</Button>
</Box>
}
Example #2
Source File: UseEquipped.tsx From genshin-optimizer with MIT License | 6 votes |
function NewItem({ onAdd, list }: { onAdd: (ck: CharacterKey) => void, list: CharacterKey[] }) { const { t } = useTranslation("page_character") const [show, onOpen, onClose] = useBoolState(false) const filter = useCallback((char?: ICachedCharacter) => { if (!char) return false return !list.includes(char.key) }, [list]) return <> <CharacterSelectionModal show={show} onHide={onClose} onSelect={onAdd} filter={filter} /> <Button fullWidth sx={{ height: itemSize }} color="info" onClick={onOpen} startIcon={<Add />} > <Trans t={t} i18nKey="tabOptimize.useEquipped.modal.add">Add character to list</Trans> </Button> </> }
Example #3
Source File: SidebarGroupList.tsx From abrechnung with GNU Affero General Public License v3.0 | 5 votes |
export default function SidebarGroupList({ group = null }) {
const groups = useRecoilValue(groupList);
const isGuest = useRecoilValue(isGuestUser);
const [showGroupCreationModal, setShowGroupCreationModal] = useState(false);
const openGroupCreateModal = () => {
setShowGroupCreationModal(true);
};
const closeGroupCreateModal = (evt, reason) => {
if (reason !== "backdropClick") {
setShowGroupCreationModal(false);
}
};
return (
<>
<List sx={{ pt: 0 }}>
<ListItem sx={{ pt: 0, pb: 0 }}>
<ListItemText secondary="Groups" />
</ListItem>
{groups.map((it) => (
<ListItemLink key={it.id} to={`/groups/${it.id}`} selected={group && group.id === it.id}>
<ListItemText primary={it.name} />
</ListItemLink>
))}
{!isGuest && (
<ListItem sx={{ padding: 0 }}>
<Grid container justifyContent="center">
<IconButton size="small" onClick={openGroupCreateModal}>
<Add />
</IconButton>
</Grid>
</ListItem>
)}
</List>
{!isGuest && <GroupCreateModal show={showGroupCreationModal} onClose={closeGroupCreateModal} />}
</>
);
}
Example #4
Source File: PurchaseDetails.tsx From abrechnung with GNU Affero General Public License v3.0 | 5 votes |
export default function PurchaseDetails({ group, transaction }) {
const [showPositions, setShowPositions] = useState(false);
return (
<>
<MobilePaper>
<TransactionActions groupID={group.id} transaction={transaction} />
<Divider sx={{ marginBottom: 1, marginTop: 1 }} />
<Grid container>
<Grid item xs={12} md={transaction.is_wip || transaction.files.length > 0 ? 6 : 12}>
<TransactionDescription group={group} transaction={transaction} />
<TransactionBilledAt group={group} transaction={transaction} />
<TransactionCreditorShare
group={group}
transaction={transaction}
isEditing={transaction.is_wip}
label="Paid by"
/>
<TransactionValue group={group} transaction={transaction} />
</Grid>
{(transaction.is_wip || transaction.files.length > 0) && (
<Grid item xs={12} md={6} sx={{ marginTop: { xs: 1 } }}>
<FileGallery transaction={transaction} />
</Grid>
)}
<Grid item xs={12}>
{transaction.is_wip ? (
<PurchaseDebitorShares
group={group}
transaction={transaction}
showPositions={showPositions}
/>
) : (
<PurchaseDebitorSharesReadOnly group={group} transaction={transaction} />
)}
</Grid>
</Grid>
</MobilePaper>
{!showPositions &&
transaction.is_wip &&
transaction.positions.find((item) => !item.deleted) === undefined ? (
<Grid container justifyContent="center" sx={{ marginTop: 2 }}>
<Button startIcon={<Add />} onClick={() => setShowPositions(true)}>
Add Positions
</Button>
</Grid>
) : (showPositions && transaction.is_wip) ||
transaction.positions.find((item) => !item.deleted) !== undefined ? (
<TransactionPositions group={group} transaction={transaction} />
) : (
<></>
)}
</>
);
}
Example #5
Source File: Chat.tsx From sapio-studio with Mozilla Public License 2.0 | 5 votes |
function Users() {
const [users, set_users] = React.useState<
{ nickname: string; key: string }[]
>([]);
React.useEffect(() => {
let cancel: ReturnType<typeof window.setTimeout>;
async function f() {
await window.electron.chat.init();
set_users(await window.electron.chat.list_users());
cancel = setTimeout(f, 5000);
}
cancel = setTimeout(f, 0);
return () => {
clearTimeout(cancel);
};
}, []);
const [add_new_user, set_add_new_user] = React.useState(false);
function CustomToolbar() {
return (
<GridToolbarContainer>
<Button onClick={() => set_add_new_user(true)}>
New User<Add></Add>
</Button>
</GridToolbarContainer>
);
}
return (
<div>
<NewNickname
show={add_new_user}
hide={() => set_add_new_user(false)}
></NewNickname>
<DataGrid
components={{ Toolbar: CustomToolbar }}
rows={users.map((v) => {
return { id: v.key, ...v };
})}
columns={UserGrid}
disableExtendRowFullWidth={false}
columnBuffer={3}
pageSize={10}
rowsPerPageOptions={[5]}
disableColumnSelector
disableSelectionOnClick
/>
</div>
);
}
Example #6
Source File: EntityCollectionView.tsx From firecms with MIT License | 4 votes |
/**
* This component is in charge of binding a datasource path with an {@link EntityCollection}
* where it's configuration is defined. It includes an infinite scrolling table,
* 'Add' new entities button,
*
* This component is the default one used for displaying entity collections
* and is in charge of generating all the specific actions and customization
* of the lower level {@link CollectionTable}
*
* Please **note** that you only need to use this component if you are building
* a custom view. If you just need to create a default view you can do it
* exclusively with config options.
*
* If you need a lower level implementation with more granular options, you
* can use {@link CollectionTable}.
*
* If you need a table that is not bound to the datasource or entities and
* properties at all, you can check {@link Table}
*
* @param path
* @param collection
* @constructor
* @category Components
*/
export function EntityCollectionView<M extends { [Key: string]: any }>({
path,
collection: baseCollection
}: EntityCollectionViewProps<M>
) {
const sideEntityController = useSideEntityController();
const context = useFireCMSContext();
const authController = useAuthController();
const navigationContext = useNavigation();
const theme = useTheme();
const largeLayout = useMediaQuery(theme.breakpoints.up("md"));
const [deleteEntityClicked, setDeleteEntityClicked] = React.useState<Entity<M> | Entity<M>[] | undefined>(undefined);
const collectionResolver = navigationContext.getCollectionResolver<M>(path);
if (!collectionResolver) {
throw Error(`Couldn't find the corresponding collection view for the path: ${path}`);
}
const onCollectionModifiedForUser = useCallback((partialCollection: PartialEntityCollection<any>) => {
navigationContext.onCollectionModifiedForUser(path, partialCollection);
}, [path]);
const collection: EntityCollection<M> = collectionResolver ?? baseCollection;
const { schemaResolver } = collectionResolver;
const exportable = collection.exportable === undefined || collection.exportable;
const selectionEnabled = collection.selectionEnabled === undefined || collection.selectionEnabled;
const hoverRow = collection.inlineEditing !== undefined && !collection.inlineEditing;
const [anchorEl, setAnchorEl] = React.useState<HTMLElement | null>(null);
const selectionController = useSelectionController<M>();
const usedSelectionController = collection.selectionController ?? selectionController;
const {
selectedEntities,
toggleEntitySelection,
isEntitySelected,
setSelectedEntities
} = usedSelectionController;
useEffect(() => {
setDeleteEntityClicked(undefined);
}, [selectedEntities]);
const onEntityClick = useCallback((entity: Entity<M>) => {
return sideEntityController.open({
entityId: entity.id,
path,
permissions: collection.permissions,
schema: collection.schema,
subcollections: collection.subcollections,
callbacks: collection.callbacks,
overrideSchemaRegistry: false
});
}, [path, collection, sideEntityController]);
const onNewClick = useCallback((e: React.MouseEvent) => {
e.stopPropagation();
return sideEntityController.open({
path,
permissions: collection.permissions,
schema: collection.schema,
subcollections: collection.subcollections,
callbacks: collection.callbacks,
overrideSchemaRegistry: false
});
}, [path, collection, sideEntityController]);
const internalOnEntityDelete = useCallback((_path: string, entity: Entity<M>) => {
setSelectedEntities(selectedEntities.filter((e) => e.id !== entity.id));
}, [selectedEntities, setSelectedEntities]);
const internalOnMultipleEntitiesDelete = useCallback((_path: string, entities: Entity<M>[]) => {
setSelectedEntities([]);
setDeleteEntityClicked(undefined);
}, [setSelectedEntities]);
const checkInlineEditing = useCallback((entity: Entity<any>) => {
if (!canEdit(collection.permissions, entity, authController, path, context)) {
return false;
}
return collection.inlineEditing === undefined || collection.inlineEditing;
}, [collection.inlineEditing, collection.permissions, path]);
const onColumnResize = useCallback(({
width,
key
}: OnColumnResizeParams) => {
// Only for property columns
if (!collection.schema.properties[key]) return;
const property: Partial<AnyProperty> = { columnWidth: width };
const updatedFields: PartialEntityCollection<any> = { schema: { properties: { [key as keyof M]: property } } };
if (onCollectionModifiedForUser)
onCollectionModifiedForUser(updatedFields)
}, [collection.schema.properties, onCollectionModifiedForUser]);
const onSizeChanged = useCallback((size: CollectionSize) => {
if (onCollectionModifiedForUser)
onCollectionModifiedForUser({ defaultSize: size })
}, [onCollectionModifiedForUser]);
const open = anchorEl != null;
const title = useMemo(() => (
<div style={{
padding: "4px"
}}>
<Typography
variant="h6"
style={{
lineHeight: "1.0",
textOverflow: "ellipsis",
whiteSpace: "nowrap",
overflow: "hidden",
maxWidth: "160px",
cursor: collection.description ? "pointer" : "inherit"
}}
onClick={collection.description
? (e) => {
setAnchorEl(e.currentTarget);
e.stopPropagation();
}
: undefined}
>
{`${collection.name}`}
</Typography>
<Typography
style={{
display: "block",
textOverflow: "ellipsis",
whiteSpace: "nowrap",
overflow: "hidden",
maxWidth: "160px",
direction: "rtl",
textAlign: "left"
}}
variant={"caption"}
color={"textSecondary"}>
{`/${path}`}
</Typography>
{collection.description &&
<Popover
id={"info-dialog"}
open={open}
anchorEl={anchorEl}
elevation={1}
onClose={() => {
setAnchorEl(null);
}}
anchorOrigin={{
vertical: "bottom",
horizontal: "center"
}}
transformOrigin={{
vertical: "top",
horizontal: "center"
}}
>
<Box m={2}>
<Markdown source={collection.description}/>
</Box>
</Popover>
}
</div>
), [collection.description, collection.name, path, open, anchorEl]);
const tableRowActionsBuilder = useCallback(({
entity,
size
}: { entity: Entity<any>, size: CollectionSize }) => {
const isSelected = isEntitySelected(entity);
const createEnabled = canCreate(collection.permissions, authController, path, context);
const editEnabled = canEdit(collection.permissions, entity, authController, path, context);
const deleteEnabled = canDelete(collection.permissions, entity, authController, path, context);
const onCopyClicked = (clickedEntity: Entity<M>) => sideEntityController.open({
entityId: clickedEntity.id,
path,
copy: true,
permissions: {
edit: editEnabled,
create: createEnabled,
delete: deleteEnabled
},
schema: collection.schema,
subcollections: collection.subcollections,
callbacks: collection.callbacks,
overrideSchemaRegistry: false
});
const onEditClicked = (clickedEntity: Entity<M>) => sideEntityController.open({
entityId: clickedEntity.id,
path,
permissions: {
edit: editEnabled,
create: createEnabled,
delete: deleteEnabled
},
schema: collection.schema,
subcollections: collection.subcollections,
callbacks: collection.callbacks,
overrideSchemaRegistry: false
});
return (
<CollectionRowActions
entity={entity}
isSelected={isSelected}
selectionEnabled={selectionEnabled}
size={size}
toggleEntitySelection={toggleEntitySelection}
onEditClicked={onEditClicked}
onCopyClicked={createEnabled ? onCopyClicked : undefined}
onDeleteClicked={deleteEnabled ? setDeleteEntityClicked : undefined}
/>
);
}, [usedSelectionController, sideEntityController, collection.permissions, authController, path]);
const toolbarActionsBuilder = useCallback((_: { size: CollectionSize, data: Entity<any>[] }) => {
const addButton = canCreate(collection.permissions, authController, path, context) && onNewClick && (largeLayout
? <Button
onClick={onNewClick}
startIcon={<Add/>}
size="large"
variant="contained"
color="primary">
Add {collection.schema.name}
</Button>
: <Button
onClick={onNewClick}
size="medium"
variant="contained"
color="primary"
>
<Add/>
</Button>);
const multipleDeleteEnabled = selectedEntities.every((entity) => canDelete(collection.permissions, entity, authController, path, context));
const onMultipleDeleteClick = (event: React.MouseEvent) => {
event.stopPropagation();
setDeleteEntityClicked(selectedEntities);
};
const multipleDeleteButton = selectionEnabled &&
<Tooltip
title={multipleDeleteEnabled ? "Multiple delete" : "You have selected one entity you cannot delete"}>
<span>
{largeLayout && <Button
disabled={!(selectedEntities?.length) || !multipleDeleteEnabled}
startIcon={<Delete/>}
onClick={onMultipleDeleteClick}
color={"primary"}
>
<p style={{ minWidth: 24 }}>({selectedEntities?.length})</p>
</Button>}
{!largeLayout &&
<IconButton
color={"primary"}
disabled={!(selectedEntities?.length) || !multipleDeleteEnabled}
onClick={onMultipleDeleteClick}
size="large">
<Delete/>
</IconButton>}
</span>
</Tooltip>;
const extraActions = collection.extraActions
? collection.extraActions({
path,
collection,
selectionController: usedSelectionController,
context
})
: undefined;
const exportButton = exportable &&
<ExportButton schema={collection.schema}
schemaResolver={schemaResolver}
exportConfig={typeof collection.exportable === "object" ? collection.exportable : undefined}
path={path}/>;
return (
<>
{extraActions}
{multipleDeleteButton}
{exportButton}
{addButton}
</>
);
}, [usedSelectionController, path, collection, largeLayout]);
return (
<>
<CollectionTable
key={`collection_table_${path}`}
title={title}
path={path}
collection={collection}
schemaResolver={schemaResolver}
onSizeChanged={onSizeChanged}
inlineEditing={checkInlineEditing}
onEntityClick={onEntityClick}
onColumnResize={onColumnResize}
tableRowActionsBuilder={tableRowActionsBuilder}
toolbarActionsBuilder={toolbarActionsBuilder}
hoverRow={hoverRow}
/>
{deleteEntityClicked && <DeleteEntityDialog entityOrEntitiesToDelete={deleteEntityClicked}
path={path}
schema={collection.schema}
schemaResolver={schemaResolver}
callbacks={collection.callbacks}
open={!!deleteEntityClicked}
onEntityDelete={internalOnEntityDelete}
onMultipleEntitiesDelete={internalOnMultipleEntitiesDelete}
onClose={() => setDeleteEntityClicked(undefined)}/>}
</>
);
}
Example #7
Source File: TransactionList.tsx From abrechnung with GNU Affero General Public License v3.0 | 4 votes |
export default function TransactionList({ group }) {
const [speedDialOpen, setSpeedDialOpen] = useState(false);
const toggleSpeedDial = () => setSpeedDialOpen((currValue) => !currValue);
const [showTransferCreateDialog, setShowTransferCreateDialog] = useState(false);
const [showPurchaseCreateDialog, setShowPurchaseCreateDialog] = useState(false);
const transactions = useRecoilValue(transactionsSeenByUser(group.id));
const currentUser = useRecoilValue(userData);
const userPermissions = useRecoilValue(currUserPermissions(group.id));
const userAccounts = useRecoilValue(accountsOwnedByUser({ groupID: group.id, userID: currentUser.id }));
const groupAccountMap = useRecoilValue(accountIDsToName(group.id));
const theme: Theme = useTheme();
const isSmallScreen = useMediaQuery(theme.breakpoints.down("md"));
const [filteredTransactions, setFilteredTransactions] = useState([]);
const [searchValue, setSearchValue] = useState("");
const [sortMode, setSortMode] = useState("last_changed"); // last_changed, description, value, billed_at
useEffect(() => {
let filtered = transactions;
if (searchValue != null && searchValue !== "") {
filtered = transactions.filter((t) => t.filter(searchValue, groupAccountMap));
}
filtered = [...filtered].sort(getTransactionSortFunc(sortMode));
setFilteredTransactions(filtered);
}, [searchValue, setFilteredTransactions, sortMode, transactions, userAccounts]);
useTitle(`${group.name} - Transactions`);
const openPurchaseCreateDialog = () => {
setShowPurchaseCreateDialog(true);
};
const openTransferCreateDialog = () => {
setShowTransferCreateDialog(true);
};
return (
<>
<MobilePaper>
<Box
sx={{
display: "flex",
flexDirection: { xs: "column", sm: "column", md: "row", lg: "row" },
alignItems: { md: "flex-end" },
pl: "16px",
justifyContent: "space-between",
}}
>
<Box sx={{ display: "flex-item" }}>
<Box sx={{ minWidth: "56px", pt: "16px" }}>
<SearchIcon sx={{ color: "action.active" }} />
</Box>
<Input
value={searchValue}
onChange={(e) => setSearchValue(e.target.value)}
placeholder="Search…"
inputProps={{
"aria-label": "search",
}}
sx={{ pt: "16px" }}
endAdornment={
<InputAdornment position="end">
<IconButton
aria-label="clear search input"
onClick={(e) => setSearchValue("")}
edge="end"
>
<Clear />
</IconButton>
</InputAdornment>
}
/>
<FormControl variant="standard" sx={{ minWidth: 120, ml: 3 }}>
<InputLabel id="select-sort-by-label">Sort by</InputLabel>
<Select
labelId="select-sort-by-label"
id="select-sort-by"
label="Sort by"
onChange={(evt) => setSortMode(evt.target.value)}
value={sortMode}
>
<MenuItem value="last_changed">Last changed</MenuItem>
<MenuItem value="description">Description</MenuItem>
<MenuItem value="value">Value</MenuItem>
<MenuItem value="billed_at">Date</MenuItem>
</Select>
</FormControl>
</Box>
{!isSmallScreen && (
<Box sx={{ display: "flex-item" }}>
<div style={{ padding: "8px" }}>
<Add color="primary" />
</div>
<Tooltip title="Create Purchase">
<IconButton color="primary" onClick={openPurchaseCreateDialog}>
<PurchaseIcon />
</IconButton>
</Tooltip>
<Tooltip title="Create Transfer">
<IconButton color="primary" onClick={openTransferCreateDialog}>
<TransferIcon />
</IconButton>
</Tooltip>
</Box>
)}
</Box>
<Divider sx={{ mt: 1 }} />
<List>
{transactions.length === 0 ? (
<Alert severity="info">No Transactions</Alert>
) : (
filteredTransactions.map((transaction) => (
<TransactionListEntry key={transaction.id} group={group} transaction={transaction} />
))
)}
</List>
<TransferCreateModal
group={group}
show={showTransferCreateDialog}
onClose={(evt, reason) => {
if (reason !== "backdropClick") {
setShowTransferCreateDialog(false);
}
}}
/>
<PurchaseCreateModal
group={group}
show={showPurchaseCreateDialog}
onClose={(evt, reason) => {
if (reason !== "backdropClick") {
setShowPurchaseCreateDialog(false);
}
}}
/>
</MobilePaper>
{userPermissions.can_write && (
<SpeedDial
ariaLabel="Create Account"
sx={{ position: "fixed", bottom: 20, right: 20 }}
icon={<SpeedDialIcon />}
// onClose={() => setSpeedDialOpen(false)}
// onOpen={() => setSpeedDialOpen(true)}
onClick={toggleSpeedDial}
open={speedDialOpen}
>
<SpeedDialAction
icon={<PurchaseIcon />}
tooltipTitle="Purchase"
tooltipOpen
onClick={openPurchaseCreateDialog}
/>
<SpeedDialAction
icon={<TransferIcon />}
tooltipTitle="Transfer"
tooltipOpen
onClick={openTransferCreateDialog}
/>
</SpeedDial>
)}
</>
);
}
Example #8
Source File: TransactionPositions.tsx From abrechnung with GNU Affero General Public License v3.0 | 4 votes |
export default function TransactionPositions({ group, transaction }: PropTypes) {
const classes = useStyles();
const accounts = useRecoilValue(accountsSeenByUser(group.id));
const [localPositionChanges, setLocalPositionChanges] = useRecoilState(
pendingTransactionPositionChanges(transaction.id)
);
const [showAdvanced, setShowAdvanced] = useState(false);
const [positions, setPositions] = useState([]);
useEffect(() => {
setPositions(
transaction.positions
.map((p) => ({ ...p, is_empty: false }))
.concat([
{
...localPositionChanges.empty,
is_empty: true,
},
])
);
}, [transaction, setPositions, localPositionChanges]);
// find all accounts that take part in the transaction, either via debitor shares or purchase items
// TODO: should we add creditor accounts as well?
const positionAccounts: Array<number> = Array.from(
new Set<number>(
positions
.map((item) => Object.keys(item.usages))
.flat()
.map((id) => parseInt(id))
)
);
const [additionalPurchaseItemAccounts, setAdditionalPurchaseItemAccounts] = useState([]);
const transactionAccounts: Array<number> = Array.from(
new Set<number>(
Object.keys(transaction.debitor_shares)
.map((id) => parseInt(id))
.concat(positionAccounts)
.concat(additionalPurchaseItemAccounts)
)
);
const showAddAccount = transactionAccounts.length < accounts.length;
const [showAccountSelect, setShowAccountSelect] = useState(false);
const totalPositionValue = positions.reduce((acc, curr) => acc + curr.price, 0);
const sharedTransactionValue = transaction.value - totalPositionValue;
const purchaseItemSumForAccount = (accountID) => {
return transaction.account_balances.hasOwnProperty(accountID)
? transaction.account_balances[accountID].positions
: 0;
};
const updatePosition = (position, name, price, communistShares) => {
if (position.is_empty) {
return updateEmptyPosition(position, name, price, communistShares);
}
if (position.only_local) {
setLocalPositionChanges((currPositions) => {
let mappedAdded = { ...currPositions.added };
mappedAdded[position.id] = {
...position,
name: name,
price: price,
communist_shares: communistShares,
};
return {
modified: currPositions.modified,
added: mappedAdded,
empty: currPositions.empty,
};
});
} else {
setLocalPositionChanges((currPositions) => {
let mappedModified = { ...currPositions.modified };
mappedModified[position.id] = {
...position,
name: name,
price: price,
communist_shares: communistShares,
};
return {
modified: mappedModified,
empty: currPositions.empty,
added: currPositions.added,
};
});
}
};
const updatePositionUsage = (position, accountID, shares) => {
if (position.is_empty) {
return updateEmptyPositionUsage(position, accountID, shares);
}
if (position.only_local) {
setLocalPositionChanges((currPositions) => {
let mappedAdded = { ...currPositions.added };
let usages = { ...currPositions.added[position.id].usages };
if (shares === 0) {
delete usages[accountID];
} else {
usages[accountID] = shares;
}
mappedAdded[position.id] = {
...currPositions.added[position.id],
usages: usages,
};
return {
modified: currPositions.modified,
added: mappedAdded,
empty: currPositions.empty,
};
});
} else {
setLocalPositionChanges((currPositions) => {
let mappedModified = { ...currPositions.modified };
let usages;
if (mappedModified.hasOwnProperty(position.id)) {
// we already did change something locally
usages = { ...currPositions.modified[position.id].usages };
} else {
// we first need to copy
usages = { ...position.usages };
}
if (shares === 0) {
delete usages[accountID];
} else {
usages[accountID] = shares;
}
mappedModified[position.id] = {
...position,
...currPositions.modified[position.id],
usages: usages,
};
return {
modified: mappedModified,
added: currPositions.added,
empty: currPositions.empty,
};
});
}
};
const deletePosition = (position) => {
if (position.is_empty) {
return resetEmptyPosition();
}
if (position.only_local) {
setLocalPositionChanges((currPositions) => {
let mappedAdded = { ...currPositions.added };
delete mappedAdded[position.id];
return {
modified: currPositions.modified,
added: mappedAdded,
empty: currPositions.empty,
};
});
} else {
setLocalPositionChanges((currPositions) => {
let mappedModified = { ...currPositions.modified };
mappedModified[position.id] = {
...position,
deleted: true,
};
return {
modified: mappedModified,
added: currPositions.added,
empty: currPositions.empty,
};
});
}
};
const nextEmptyPositionID = (localPositions: LocalPositionChanges) => {
return Math.min(...Object.values(localPositions.added).map((p) => p.id), -1, localPositions.empty.id) - 1;
};
const resetEmptyPosition = () => {
setLocalPositionChanges((currValue) => ({
modified: currValue.modified,
added: currValue.added,
empty: {
id: nextEmptyPositionID(currValue),
name: "",
price: 0,
communist_shares: 0,
usages: {},
deleted: false,
},
}));
};
const updateEmptyPosition = (position, name, price, communistShares) => {
if (name !== "" && name != null) {
const copyOfEmpty = { ...position, name: name, price: price, communist_shares: communistShares };
setLocalPositionChanges((currPositions) => {
let mappedAdded = { ...currPositions.added };
mappedAdded[position.id] = copyOfEmpty;
return {
modified: currPositions.modified,
added: mappedAdded,
empty: {
id: nextEmptyPositionID(currPositions),
name: "",
price: 0,
communist_shares: 0,
usages: {},
deleted: false,
},
};
});
} else {
setLocalPositionChanges((currPositions) => {
return {
modified: currPositions.modified,
added: currPositions.added,
empty: {
...position,
name: name,
price: price,
communist_shares: communistShares,
},
};
});
}
};
const updateEmptyPositionUsage = (position, accountID, value) => {
setLocalPositionChanges((currPositions) => {
let newUsages = { ...position.usages };
if (value === 0) {
delete newUsages[accountID];
} else {
newUsages[accountID] = value;
}
return {
modified: currPositions.modified,
added: currPositions.added,
empty: {
...position,
usages: newUsages,
},
};
});
};
const copyPosition = (position) => {
setLocalPositionChanges((currPositions) => {
const newPosition = {
...position,
id: nextEmptyPositionID(currPositions),
};
let mappedAdded = { ...currPositions.added };
mappedAdded[newPosition.id] = newPosition;
return {
modified: currPositions.modified,
added: mappedAdded,
empty: currPositions.empty,
};
});
};
const addPurchaseItemAccount = (account) => {
setShowAccountSelect(false);
setAdditionalPurchaseItemAccounts((currAdditionalAccounts) =>
Array.from(new Set<number>([...currAdditionalAccounts, parseInt(account.id)]))
);
};
return (
<MobilePaper sx={{ marginTop: 2 }}>
<Grid container direction="row" justifyContent="space-between">
<Typography>Positions</Typography>
{transaction.is_wip && (
<FormControlLabel
control={<Checkbox name={`show-advanced`} />}
checked={showAdvanced}
onChange={(event: React.ChangeEvent<HTMLInputElement>) => setShowAdvanced(event.target.checked)}
label="Advanced"
/>
)}
</Grid>
<TableContainer>
<Table className={classes.table} stickyHeader aria-label="purchase items" size="small">
<TableHead>
<TableRow>
<TableCell>Name</TableCell>
<TableCell align="right">Price</TableCell>
{(transaction.is_wip ? transactionAccounts : positionAccounts).map((accountID) => (
<TableCell align="right" sx={{ minWidth: 80 }} key={accountID}>
{accounts.find((account) => account.id === accountID).name}
</TableCell>
))}
{transaction.is_wip && (
<>
{showAccountSelect && (
<TableCell align="right">
<AccountSelect
group={group}
exclude={transactionAccounts}
onChange={addPurchaseItemAccount}
/>
</TableCell>
)}
{showAddAccount && (
<TableCell align="right">
<IconButton onClick={() => setShowAccountSelect(true)}>
<Add />
</IconButton>
</TableCell>
)}
</>
)}
<TableCell align="right">Shared</TableCell>
{transaction.is_wip && <TableCell></TableCell>}
</TableRow>
</TableHead>
<TableBody>
{transaction.is_wip
? positions.map((position, idx) => (
<TableRow hover key={position.id}>
<PositionTableRow
position={position}
deletePosition={deletePosition}
transactionAccounts={transactionAccounts}
copyPosition={copyPosition}
updatePosition={updatePosition}
updatePositionUsage={updatePositionUsage}
showAdvanced={showAdvanced}
showAccountSelect={showAccountSelect}
showAddAccount={showAddAccount}
/>
</TableRow>
))
: positions.map(
(position) =>
!position.is_empty && (
<TableRow hover key={position.id}>
<TableCell>{position.name}</TableCell>
<TableCell align="right" style={{ minWidth: 80 }}>
{position.price.toFixed(2)} {transaction.currency_symbol}
</TableCell>
{positionAccounts.map((accountID) => (
<TableCell align="right" key={accountID}>
{position.usages.hasOwnProperty(String(accountID))
? position.usages[String(accountID)]
: 0}
</TableCell>
))}
<TableCell align="right">{position.communist_shares}</TableCell>
</TableRow>
)
)}
<TableRow hover>
<TableCell>
<Typography sx={{ fontWeight: "bold" }}>Total:</Typography>
</TableCell>
<TableCell align="right">
{totalPositionValue.toFixed(2)} {transaction.currency_symbol}
</TableCell>
{(transaction.is_wip ? transactionAccounts : positionAccounts).map((accountID) => (
<TableCell align="right" key={accountID}>
{purchaseItemSumForAccount(accountID).toFixed(2)} {transaction.currency_symbol}
</TableCell>
))}
<TableCell align="right" colSpan={showAddAccount ? 2 : 1}>
{(
positions.reduce((acc, curr) => acc + curr.price, 0) -
Object.values(transaction.account_balances).reduce(
(acc, curr) => acc + curr.positions,
0
)
).toFixed(2)}{" "}
{transaction.currency_symbol}
</TableCell>
{transaction.is_wip && <TableCell></TableCell>}
</TableRow>
<TableRow hover>
<TableCell>
<Typography sx={{ fontWeight: "bold" }}>Remaining:</Typography>
</TableCell>
<TableCell align="right">
{sharedTransactionValue.toFixed(2)} {transaction.currency_symbol}
</TableCell>
{(transaction.is_wip ? transactionAccounts : positionAccounts).map((accountID) => (
<TableCell align="right" key={accountID}></TableCell>
))}
<TableCell align="right" colSpan={showAddAccount ? 2 : 1}></TableCell>
{transaction.is_wip && <TableCell></TableCell>}
</TableRow>
</TableBody>
</Table>
</TableContainer>
</MobilePaper>
);
}
Example #9
Source File: AccountList.tsx From abrechnung with GNU Affero General Public License v3.0 | 4 votes |
export default function AccountList({ group }) {
const [speedDialOpen, setSpeedDialOpen] = useState(false);
const toggleSpeedDial = () => setSpeedDialOpen((currValue) => !currValue);
const [showPersonalAccountCreationModal, setShowPersonalAccountCreationModal] = useState(false);
const [showClearingAccountCreationModal, setShowClearingAccountCreationModal] = useState(false);
const [activeTab, setActiveTab] = useState("personal");
const [searchValuePersonal, setSearchValuePersonal] = useState("");
const [searchValueClearing, setSearchValueClearing] = useState("");
const [showPersonalAccountEditModal, setShowPersonalAccountEditModal] = useState(false);
const [showClearingAccountEditModal, setShowClearingAccountEditModal] = useState(false);
const [clearingAccountToCopy, setClearingAccountToCopy] = useState(undefined);
const [accountToEdit, setAccountToEdit] = useState(null);
const [clearingAccountToEdit, setClearingAccountToEdit] = useState(null);
const setAccounts = useSetRecoilState(groupAccounts(group.id));
const personalAccounts = useRecoilValue(personalAccountsSeenByUser(group.id));
const clearingAccounts = useRecoilValue(clearingAccountsSeenByUser(group.id));
const allAccounts = useRecoilValue(accountsSeenByUser(group.id));
const [accountToDelete, setAccountToDelete] = useState(null);
const userPermissions = useRecoilValue(currUserPermissions(group.id));
const currentUser = useRecoilValue(userData);
const memberIDToUsername = useRecoilValue(groupMemberIDsToUsername(group.id));
const [filteredPersonalAccounts, setFilteredPersonalAccounts] = useState([]);
const [filteredClearingAccounts, setFilteredClearingAccounts] = useState([]);
useEffect(() => {
if (searchValuePersonal != null && searchValuePersonal !== "") {
setFilteredPersonalAccounts(
personalAccounts.filter((t) => {
return (
t.name.toLowerCase().includes(searchValuePersonal.toLowerCase()) ||
t.description.toLowerCase().includes(searchValuePersonal.toLowerCase())
);
})
);
} else {
return setFilteredPersonalAccounts(personalAccounts);
}
}, [personalAccounts, searchValuePersonal, setFilteredPersonalAccounts]);
useEffect(() => {
if (searchValueClearing != null && searchValueClearing !== "") {
setFilteredClearingAccounts(
clearingAccounts.filter((t) => {
return (
t.name.toLowerCase().includes(searchValueClearing.toLowerCase()) ||
t.description.toLowerCase().includes(searchValueClearing.toLowerCase())
);
})
);
} else {
return setFilteredClearingAccounts(clearingAccounts);
}
}, [clearingAccounts, searchValueClearing, setFilteredClearingAccounts]);
useTitle(`${group.name} - Accounts`);
const openAccountEdit = (account) => {
setAccountToEdit(account);
setShowPersonalAccountEditModal(true);
};
const closeAccountEdit = (evt, reason) => {
if (reason !== "backdropClick") {
setShowPersonalAccountEditModal(false);
setAccountToEdit(null);
}
};
const openClearingAccountEdit = (account) => {
setClearingAccountToEdit(account);
setShowClearingAccountEditModal(true);
};
const closeClearingAccountEdit = (evt, reason) => {
if (reason !== "backdropClick") {
setShowClearingAccountEditModal(false);
setClearingAccountToEdit(null);
}
};
const confirmDeleteAccount = () => {
if (accountToDelete !== null) {
deleteAccount({ accountID: accountToDelete })
.then((account) => {
updateAccount(account, setAccounts);
setAccountToDelete(null);
})
.catch((err) => {
toast.error(err);
});
}
};
const openCreateDialog = () => {
setClearingAccountToCopy(undefined);
setShowClearingAccountCreationModal(true);
};
const copyClearingAccount = (account) => {
setClearingAccountToCopy(account);
setShowClearingAccountCreationModal(true);
};
return (
<>
<MobilePaper>
<TabContext value={activeTab}>
<Box sx={{ borderBottom: 1, borderColor: "divider" }}>
<TabList onChange={(e, newValue) => setActiveTab(newValue)} centered>
<Tab
value="personal"
label={
<TextBadge badgeContent={personalAccounts.length} color="primary">
<span>Personal Accounts</span>
</TextBadge>
}
/>
<Tab
label={
<TextBadge badgeContent={clearingAccounts.length} color="primary">
<span>Clearing Accounts</span>
</TextBadge>
}
value="clearing"
/>
</TabList>
</Box>
<TabPanel value="personal">
<List>
{personalAccounts.length === 0 ? (
<Alert severity="info">No Accounts</Alert>
) : (
<>
<ListItem>
<Input
value={searchValuePersonal}
onChange={(e) => setSearchValuePersonal(e.target.value)}
placeholder="Search…"
inputProps={{
"aria-label": "search",
}}
endAdornment={
<InputAdornment position="end">
<IconButton
aria-label="clear search input"
onClick={(e) => setSearchValuePersonal("")}
edge="end"
>
<Clear />
</IconButton>
</InputAdornment>
}
/>
</ListItem>
<Divider />
{filteredPersonalAccounts.map((account) => (
<ListItem sx={{ padding: 0 }} key={account.id}>
<ListItemLink to={`/groups/${group.id}/accounts/${account.id}`}>
<ListItemText
primary={
<div>
<span>{account.name}</span>
{account.owning_user_id === currentUser.id ? (
<span>
, owned by{" "}
<Chip
size="small"
component="span"
color="primary"
label="you"
/>
</span>
) : (
account.owning_user_id !== null && (
<span>
, owned by{" "}
<Chip
size="small"
component="span"
color="secondary"
label={
memberIDToUsername[
account.owning_user_id
]
}
/>
</span>
)
)}
</div>
}
secondary={account.description}
/>
</ListItemLink>
{userPermissions.can_write && (
<ListItemSecondaryAction>
<IconButton
color="primary"
onClick={() => openAccountEdit(account)}
>
<Edit />
</IconButton>
<IconButton
color="error"
onClick={() => setAccountToDelete(account.id)}
>
<Delete />
</IconButton>
</ListItemSecondaryAction>
)}
</ListItem>
))}
</>
)}
</List>
{userPermissions.can_write && (
<>
<Grid container justifyContent="center">
<Tooltip title="Create Personal Account">
<IconButton
color="primary"
onClick={() => setShowPersonalAccountCreationModal(true)}
>
<Add />
</IconButton>
</Tooltip>
</Grid>
<CreateAccountModal
show={showPersonalAccountCreationModal}
onClose={(evt, reason) => {
if (reason !== "backdropClick") {
setShowPersonalAccountCreationModal(false);
}
}}
group={group}
/>
<EditAccountModal
show={showPersonalAccountEditModal}
onClose={closeAccountEdit}
account={accountToEdit}
group={group}
/>
</>
)}
</TabPanel>
<TabPanel value="clearing">
<List>
{clearingAccounts.length === 0 ? (
<Alert severity="info">No Accounts</Alert>
) : (
<>
<ListItem>
<Input
value={searchValueClearing}
onChange={(e) => setSearchValueClearing(e.target.value)}
placeholder="Search…"
inputProps={{
"aria-label": "search",
}}
endAdornment={
<InputAdornment position="end">
<IconButton
aria-label="clear search input"
onClick={(e) => setSearchValueClearing("")}
edge="end"
>
<Clear />
</IconButton>
</InputAdornment>
}
/>
</ListItem>
<Divider />
{filteredClearingAccounts.map((account) => (
<ListItem sx={{ padding: 0 }} key={account.id}>
<ListItemLink to={`/groups/${group.id}/accounts/${account.id}`}>
<ListItemText primary={account.name} secondary={account.description} />
</ListItemLink>
{userPermissions.can_write && (
<ListItemSecondaryAction>
<IconButton
color="primary"
onClick={() => openClearingAccountEdit(account)}
>
<Edit />
</IconButton>
<IconButton
color="primary"
onClick={() => copyClearingAccount(account)}
>
<ContentCopy />
</IconButton>
<IconButton
color="error"
onClick={() => setAccountToDelete(account.id)}
>
<Delete />
</IconButton>
</ListItemSecondaryAction>
)}
</ListItem>
))}
</>
)}
</List>
{userPermissions.can_write && (
<>
<Grid container justifyContent="center">
<Tooltip title="Create Clearing Account">
<IconButton color="primary" onClick={openCreateDialog}>
<Add />
</IconButton>
</Tooltip>
</Grid>
<CreateClearingAccountModal
show={showClearingAccountCreationModal}
onClose={(evt, reason) => {
if (reason !== "backdropClick") {
setShowClearingAccountCreationModal(false);
}
}}
initialValues={clearingAccountToCopy}
group={group}
/>
<EditClearingAccountModal
show={showClearingAccountEditModal}
onClose={closeClearingAccountEdit}
account={clearingAccountToEdit}
group={group}
/>
</>
)}
</TabPanel>
</TabContext>
</MobilePaper>
{userPermissions.can_write && (
<>
<SpeedDial
ariaLabel="Create Account"
sx={{ position: "fixed", bottom: 20, right: 20 }}
icon={<SpeedDialIcon />}
// onClose={() => setSpeedDialOpen(false)}
// onOpen={() => setSpeedDialOpen(true)}
onClick={toggleSpeedDial}
open={speedDialOpen}
>
<SpeedDialAction
icon={<PersonalAccountIcon />}
tooltipTitle="Personal"
tooltipOpen
onClick={() => setShowPersonalAccountCreationModal(true)}
/>
<SpeedDialAction
icon={<ClearingAccountIcon />}
tooltipTitle="Clearing"
tooltipOpen
onClick={openCreateDialog}
/>
</SpeedDial>
<Dialog maxWidth="xs" aria-labelledby="confirmation-dialog-title" open={accountToDelete !== null}>
<DialogTitle id="confirmation-dialog-title">Confirm delete account</DialogTitle>
<DialogContent dividers>
Are you sure you want to delete the account "
{allAccounts.find((acc) => acc.id === accountToDelete)?.name}"
</DialogContent>
<DialogActions>
<Button autoFocus onClick={() => setAccountToDelete(null)} color="primary">
Cancel
</Button>
<Button onClick={confirmDeleteAccount} color="error">
Ok
</Button>
</DialogActions>
</Dialog>
</>
)}
</>
);
}
Example #10
Source File: GroupInvites.tsx From abrechnung with GNU Affero General Public License v3.0 | 4 votes |
export default function GroupInvites({ group }) {
const [showModal, setShowModal] = useState(false);
const invites = useRecoilValue(groupInvites(group.id));
const members = useRecoilValue(groupMembers(group.id));
const userPermissions = useRecoilValue(currUserPermissions(group.id));
const isGuest = useRecoilValue(isGuestUser);
useTitle(`${group.name} - Invite Links`);
const deleteToken = (id) => {
deleteGroupInvite({ groupID: group.id, inviteID: id }).catch((err) => {
toast.error(err);
});
};
const getMemberUsername = (member_id) => {
const member = members.find((member) => member.user_id === member_id);
if (member === undefined) {
return "unknown";
}
return member.username;
};
const selectLink = (event) => {
const node = event.target;
const selection = window.getSelection();
const range = document.createRange();
range.selectNodeContents(node);
selection.removeAllRanges();
selection.addRange(range);
};
const copyToClipboard = (content) => {
navigator.clipboard.writeText(content);
toast.info("Link copied to clipboard!");
};
return (
<MobilePaper>
<Typography component="h3" variant="h5">
Active Invite Links
</Typography>
{isGuest && (
<Alert severity="info">
You are a guest user on this Abrechnung and therefore not permitted to create group invites.
</Alert>
)}
<List>
{invites.length === 0 ? (
<ListItem>
<ListItemText primary="No Links" />
</ListItem>
) : (
invites.map((invite) => (
<ListItem key={invite.id}>
<ListItemText
primary={
invite.token === null ? (
<span>token hidden, was created by another member</span>
) : (
<span onClick={selectLink}>
{window.location.origin}/invite/
{invite.token}
</span>
)
}
secondary={
<>
{invite.description}, created by {getMemberUsername(invite.created_by)}, valid
until{" "}
{DateTime.fromISO(invite.valid_until).toLocaleString(DateTime.DATETIME_FULL)}
{invite.single_use && ", single use"}
{invite.join_as_editor && ", join as editor"}
</>
}
/>
{userPermissions.can_write && (
<ListItemSecondaryAction>
<IconButton
color="primary"
onClick={() =>
copyToClipboard(`${window.location.origin}/invite/${invite.token}`)
}
>
<ContentCopy />
</IconButton>
<IconButton color="error" onClick={() => deleteToken(invite.id)}>
<Delete />
</IconButton>
</ListItemSecondaryAction>
)}
</ListItem>
))
)}
</List>
{userPermissions.can_write && !isGuest && (
<>
<Grid container justifyContent="center">
<IconButton color="primary" onClick={() => setShowModal(true)}>
<Add />
</IconButton>
</Grid>
<InviteLinkCreate show={showModal} onClose={() => setShowModal(false)} group={group} />
</>
)}
</MobilePaper>
);
}
Example #11
Source File: GroupList.tsx From abrechnung with GNU Affero General Public License v3.0 | 4 votes |
export default function GroupList() {
const [showGroupCreationModal, setShowGroupCreationModal] = useState(false);
const [showGroupDeletionModal, setShowGroupDeletionModal] = useState(false);
const [groupToDelete, setGroupToDelete] = useState(null);
const groups = useRecoilValue(groupList);
const isGuest = useRecoilValue(isGuestUser);
const openGroupDeletionModal = (groupID) => {
setGroupToDelete(groups.find((group) => group.id === groupID));
setShowGroupDeletionModal(true);
};
const closeGroupDeletionModal = () => {
setShowGroupDeletionModal(false);
setGroupToDelete(null);
};
const openGroupCreateModal = () => {
setShowGroupCreationModal(true);
};
const closeGroupCreateModal = (evt, reason) => {
if (reason !== "backdropClick") {
setShowGroupCreationModal(false);
}
};
return (
<MobilePaper>
<Typography component="h3" variant="h5">
Groups
</Typography>
{isGuest && (
<Alert severity="info">
You are a guest user on this Abrechnung and therefore not permitted to create new groups.
</Alert>
)}
<List>
{groups.length === 0 ? (
<ListItem key={0}>
<span>No Groups</span>
</ListItem>
) : (
groups.map((group) => {
return (
<ListItem sx={{ padding: 0 }} key={group.id}>
<ListItemLink to={`/groups/${group.id}`}>
<ListItemText primary={group.name} secondary={group.description} />
</ListItemLink>
<ListItemSecondaryAction>
<IconButton
edge="end"
aria-label="delete-group"
onClick={() => openGroupDeletionModal(group.id)}
>
<Delete />
</IconButton>
</ListItemSecondaryAction>
</ListItem>
);
})
)}
</List>
{!isGuest && (
<>
<Grid container justifyContent="center">
<IconButton color="primary" onClick={openGroupCreateModal}>
<Add />
</IconButton>
</Grid>
<GroupCreateModal show={showGroupCreationModal} onClose={closeGroupCreateModal} />
</>
)}
<GroupDeleteModal
show={showGroupDeletionModal}
onClose={closeGroupDeletionModal}
groupToDelete={groupToDelete}
/>
</MobilePaper>
);
}
Example #12
Source File: ArtifactEditor.tsx From genshin-optimizer with MIT License | 4 votes |
export default function ArtifactEditor({ artifactIdToEdit = "", cancelEdit, allowUpload = false, allowEmpty = false, disableEditSetSlot: disableEditSlotProp = false }:
{ artifactIdToEdit?: string, cancelEdit: () => void, allowUpload?: boolean, allowEmpty?: boolean, disableEditSetSlot?: boolean }) {
const { t } = useTranslation("artifact")
const artifactSheets = usePromise(ArtifactSheet.getAll, [])
const { database } = useContext(DatabaseContext)
const [show, setShow] = useState(false)
const [dirtyDatabase, setDirtyDatabase] = useForceUpdate()
useEffect(() => database.followAnyArt(setDirtyDatabase), [database, setDirtyDatabase])
const [editorArtifact, artifactDispatch] = useReducer(artifactReducer, undefined)
const artifact = useMemo(() => editorArtifact && parseArtifact(editorArtifact), [editorArtifact])
const [modalShow, setModalShow] = useState(false)
const [{ processed, outstanding }, dispatchQueue] = useReducer(queueReducer, { processed: [], outstanding: [] })
const firstProcessed = processed[0] as ProcessedEntry | undefined
const firstOutstanding = outstanding[0] as OutstandingEntry | undefined
const processingImageURL = usePromise(firstOutstanding?.imageURL, [firstOutstanding?.imageURL])
const processingResult = usePromise(firstOutstanding?.result, [firstOutstanding?.result])
const remaining = processed.length + outstanding.length
const image = firstProcessed?.imageURL ?? processingImageURL
const { artifact: artifactProcessed, texts } = firstProcessed ?? {}
// const fileName = firstProcessed?.fileName ?? firstOutstanding?.fileName ?? "Click here to upload Artifact screenshot files"
const disableEditSetSlot = disableEditSlotProp || !!artifact?.location
useEffect(() => {
if (!artifact && artifactProcessed)
artifactDispatch({ type: "overwrite", artifact: artifactProcessed })
}, [artifact, artifactProcessed, artifactDispatch])
useEffect(() => {
const numProcessing = Math.min(maxProcessedCount - processed.length, maxProcessingCount, outstanding.length)
const processingCurrent = numProcessing && !outstanding[0].result
outstanding.slice(0, numProcessing).forEach(processEntry)
if (processingCurrent)
dispatchQueue({ type: "processing" })
}, [processed.length, outstanding])
useEffect(() => {
if (processingResult)
dispatchQueue({ type: "processed", ...processingResult })
}, [processingResult, dispatchQueue])
const uploadFiles = useCallback((files: FileList) => {
setShow(true)
dispatchQueue({ type: "upload", files: [...files].map(file => ({ file, fileName: file.name })) })
}, [dispatchQueue, setShow])
const clearQueue = useCallback(() => dispatchQueue({ type: "clear" }), [dispatchQueue])
useEffect(() => {
const pasteFunc = (e: any) => uploadFiles(e.clipboardData.files)
allowUpload && window.addEventListener('paste', pasteFunc);
return () => {
if (allowUpload) window.removeEventListener('paste', pasteFunc)
}
}, [uploadFiles, allowUpload])
const onUpload = useCallback(
e => {
uploadFiles(e.target.files)
e.target.value = null // reset the value so the same file can be uploaded again...
},
[uploadFiles],
)
const { old, oldType }: { old: ICachedArtifact | undefined, oldType: "edit" | "duplicate" | "upgrade" | "" } = useMemo(() => {
const databaseArtifact = dirtyDatabase && artifactIdToEdit && database._getArt(artifactIdToEdit)
if (databaseArtifact) return { old: databaseArtifact, oldType: "edit" }
if (artifact === undefined) return { old: undefined, oldType: "" }
const { duplicated, upgraded } = dirtyDatabase && database.findDuplicates(artifact)
return { old: duplicated[0] ?? upgraded[0], oldType: duplicated.length !== 0 ? "duplicate" : "upgrade" }
}, [artifact, artifactIdToEdit, database, dirtyDatabase])
const { artifact: cachedArtifact, errors } = useMemo(() => {
if (!artifact) return { artifact: undefined, errors: [] as Displayable[] }
const validated = validateArtifact(artifact, artifactIdToEdit)
if (old) {
validated.artifact.location = old.location
validated.artifact.exclude = old.exclude
}
return validated
}, [artifact, artifactIdToEdit, old])
// Overwriting using a different function from `databaseArtifact` because `useMemo` does not
// guarantee to trigger *only when* dependencies change, which is necessary in this case.
useEffect(() => {
if (artifactIdToEdit === "new") {
setShow(true)
artifactDispatch({ type: "reset" })
}
const databaseArtifact = artifactIdToEdit && dirtyDatabase && database._getArt(artifactIdToEdit)
if (databaseArtifact) {
setShow(true)
artifactDispatch({ type: "overwrite", artifact: deepClone(databaseArtifact) })
}
}, [artifactIdToEdit, database, dirtyDatabase])
const sheet = artifact ? artifactSheets?.[artifact.setKey] : undefined
const reset = useCallback(() => {
cancelEdit?.();
dispatchQueue({ type: "pop" })
artifactDispatch({ type: "reset" })
}, [cancelEdit, artifactDispatch])
const update = useCallback((newValue: Partial<IArtifact>) => {
const newSheet = newValue.setKey ? artifactSheets![newValue.setKey] : sheet!
function pick<T>(value: T | undefined, available: readonly T[], prefer?: T): T {
return (value && available.includes(value)) ? value : (prefer ?? available[0])
}
if (newValue.setKey) {
newValue.rarity = pick(artifact?.rarity, newSheet.rarity, Math.max(...newSheet.rarity) as ArtifactRarity)
newValue.slotKey = pick(artifact?.slotKey, newSheet.slots)
}
if (newValue.rarity)
newValue.level = artifact?.level ?? 0
if (newValue.level)
newValue.level = clamp(newValue.level, 0, 4 * (newValue.rarity ?? artifact!.rarity))
if (newValue.slotKey)
newValue.mainStatKey = pick(artifact?.mainStatKey, Artifact.slotMainStats(newValue.slotKey))
if (newValue.mainStatKey) {
newValue.substats = [0, 1, 2, 3].map(i =>
(artifact && artifact.substats[i].key !== newValue.mainStatKey) ? artifact!.substats[i] : { key: "", value: 0 })
}
artifactDispatch({ type: "update", artifact: newValue })
}, [artifact, artifactSheets, sheet, artifactDispatch])
const setSubstat = useCallback((index: number, substat: ISubstat) => {
artifactDispatch({ type: "substat", index, substat })
}, [artifactDispatch])
const isValid = !errors.length
const canClearArtifact = (): boolean => window.confirm(t`editor.clearPrompt` as string)
const { rarity = 5, level = 0, slotKey = "flower" } = artifact ?? {}
const { currentEfficiency = 0, maxEfficiency = 0 } = cachedArtifact ? Artifact.getArtifactEfficiency(cachedArtifact, allSubstatFilter) : {}
const preventClosing = processed.length || outstanding.length
const onClose = useCallback(
(e) => {
if (preventClosing) e.preventDefault()
setShow(false)
cancelEdit()
}, [preventClosing, setShow, cancelEdit])
const theme = useTheme();
const grmd = useMediaQuery(theme.breakpoints.up('md'));
const element = artifact ? allElementsWithPhy.find(ele => artifact.mainStatKey.includes(ele)) : undefined
const color = artifact
? element ?? "success"
: "primary"
return <ModalWrapper open={show} onClose={onClose} >
<Suspense fallback={<Skeleton variant="rectangular" sx={{ width: "100%", height: show ? "100%" : 64 }} />}><CardDark >
<UploadExplainationModal modalShow={modalShow} hide={() => setModalShow(false)} />
<CardHeader
title={<Trans t={t} i18nKey="editor.title" >Artifact Editor</Trans>}
action={<CloseButton disabled={!!preventClosing} onClick={onClose} />}
/>
<CardContent sx={{ display: "flex", flexDirection: "column", gap: 1 }}>
<Grid container spacing={1} columns={{ xs: 1, md: 2 }} >
{/* Left column */}
<Grid item xs={1} display="flex" flexDirection="column" gap={1}>
{/* set & rarity */}
<ButtonGroup sx={{ display: "flex", mb: 1 }}>
{/* Artifact Set */}
<ArtifactSetSingleAutocomplete
size="small"
disableClearable
artSetKey={artifact?.setKey ?? ""}
setArtSetKey={setKey => update({ setKey: setKey as ArtifactSetKey })}
sx={{ flexGrow: 1 }}
disabled={disableEditSetSlot}
/>
{/* rarity dropdown */}
<ArtifactRarityDropdown rarity={artifact ? rarity : undefined} onChange={r => update({ rarity: r })} filter={r => !!sheet?.rarity?.includes?.(r)} disabled={disableEditSetSlot || !sheet} />
</ButtonGroup>
{/* level */}
<Box component="div" display="flex">
<CustomNumberTextField id="filled-basic" label="Level" variant="filled" sx={{ flexShrink: 1, flexGrow: 1, mr: 1, my: 0 }} margin="dense" size="small"
value={level} disabled={!sheet} placeholder={`0~${rarity * 4}`} onChange={l => update({ level: l })}
/>
<ButtonGroup >
<Button onClick={() => update({ level: level - 1 })} disabled={!sheet || level === 0}>-</Button>
{rarity ? [...Array(rarity + 1).keys()].map(i => 4 * i).map(i => <Button key={i} onClick={() => update({ level: i })} disabled={!sheet || level === i}>{i}</Button>) : null}
<Button onClick={() => update({ level: level + 1 })} disabled={!sheet || level === (rarity * 4)}>+</Button>
</ButtonGroup>
</Box>
{/* slot */}
<Box component="div" display="flex">
<ArtifactSlotDropdown disabled={disableEditSetSlot || !sheet} slotKey={slotKey} onChange={slotKey => update({ slotKey })} />
<CardLight sx={{ p: 1, ml: 1, flexGrow: 1 }}>
<Suspense fallback={<Skeleton width="60%" />}>
<Typography color="text.secondary">
{sheet?.getSlotName(artifact!.slotKey) ? <span><ImgIcon src={sheet.slotIcons[artifact!.slotKey]} /> {sheet?.getSlotName(artifact!.slotKey)}</span> : t`editor.unknownPieceName`}
</Typography>
</Suspense>
</CardLight>
</Box>
{/* main stat */}
<Box component="div" display="flex">
<DropdownButton startIcon={element ? uncoloredEleIcons[element] : (artifact?.mainStatKey ? StatIcon[artifact.mainStatKey] : undefined)}
title={<b>{artifact ? KeyMap.getArtStr(artifact.mainStatKey) : t`mainStat`}</b>} disabled={!sheet} color={color} >
{Artifact.slotMainStats(slotKey).map(mainStatK =>
<MenuItem key={mainStatK} selected={artifact?.mainStatKey === mainStatK} disabled={artifact?.mainStatKey === mainStatK} onClick={() => update({ mainStatKey: mainStatK })} >
<ListItemIcon>{StatIcon[mainStatK]}</ListItemIcon>
<ListItemText>{KeyMap.getArtStr(mainStatK)}</ListItemText>
</MenuItem>)}
</DropdownButton>
<CardLight sx={{ p: 1, ml: 1, flexGrow: 1 }}>
<Typography color="text.secondary">
{artifact ? `${cacheValueString(Artifact.mainStatValue(artifact.mainStatKey, rarity, level), KeyMap.unit(artifact.mainStatKey))}${KeyMap.unit(artifact.mainStatKey)}` : t`mainStat`}
</Typography>
</CardLight>
</Box>
{/* Current/Max Substats Efficiency */}
<SubstatEfficiencyDisplayCard valid={isValid} efficiency={currentEfficiency} t={t} />
{currentEfficiency !== maxEfficiency && <SubstatEfficiencyDisplayCard max valid={isValid} efficiency={maxEfficiency} t={t} />}
{/* Image OCR */}
{allowUpload && <CardLight>
<CardContent sx={{ display: "flex", flexDirection: "column", gap: 1 }}>
{/* TODO: artifactDispatch not overwrite */}
<Suspense fallback={<Skeleton width="100%" height="100" />}>
<Grid container spacing={1} alignItems="center">
<Grid item flexGrow={1}>
<label htmlFor="contained-button-file">
<InputInvis accept="image/*" id="contained-button-file" multiple type="file" onChange={onUpload} />
<Button component="span" startIcon={<PhotoCamera />}>
Upload Screenshot (or Ctrl-V)
</Button>
</label>
</Grid>
<Grid item>
<Button color="info" sx={{ px: 2, minWidth: 0 }} onClick={() => setModalShow(true)}><Typography><FontAwesomeIcon icon={faQuestionCircle} /></Typography></Button>
</Grid>
</Grid>
{image && <Box display="flex" justifyContent="center">
<Box component="img" src={image} width="100%" maxWidth={350} height="auto" alt="Screenshot to parse for artifact values" />
</Box>}
{remaining > 0 && <CardDark sx={{ pl: 2 }} ><Grid container spacing={1} alignItems="center" >
{!firstProcessed && firstOutstanding && <Grid item>
<CircularProgress size="1em" />
</Grid>}
<Grid item flexGrow={1}>
<Typography>
<span>
Screenshots in file-queue: <b>{remaining}</b>
{/* {process.env.NODE_ENV === "development" && ` (Debug: Processed ${processed.length}/${maxProcessedCount}, Processing: ${outstanding.filter(entry => entry.result).length}/${maxProcessingCount}, Outstanding: ${outstanding.length})`} */}
</span>
</Typography>
</Grid>
<Grid item>
<Button size="small" color="error" onClick={clearQueue}>Clear file-queue</Button>
</Grid>
</Grid></CardDark>}
</Suspense>
</CardContent>
</CardLight>}
</Grid>
{/* Right column */}
<Grid item xs={1} display="flex" flexDirection="column" gap={1}>
{/* substat selections */}
{[0, 1, 2, 3].map((index) => <SubstatInput key={index} index={index} artifact={cachedArtifact} setSubstat={setSubstat} />)}
{texts && <CardLight><CardContent>
<div>{texts.slotKey}</div>
<div>{texts.mainStatKey}</div>
<div>{texts.mainStatVal}</div>
<div>{texts.rarity}</div>
<div>{texts.level}</div>
<div>{texts.substats}</div>
<div>{texts.setKey}</div>
</CardContent></CardLight>}
</Grid>
</Grid>
{/* Duplicate/Updated/Edit UI */}
{old && <Grid container sx={{ justifyContent: "space-around" }} spacing={1} >
<Grid item xs={12} md={5.5} lg={4} ><CardLight>
<Typography sx={{ textAlign: "center" }} py={1} variant="h6" color="text.secondary" >{oldType !== "edit" ? (oldType === "duplicate" ? t`editor.dupArt` : t`editor.upArt`) : t`editor.beforeEdit`}</Typography>
<ArtifactCard artifactObj={old} />
</CardLight></Grid>
{grmd && <Grid item md={1} display="flex" alignItems="center" justifyContent="center" >
<CardLight sx={{ display: "flex" }}><ChevronRight sx={{ fontSize: 40 }} /></CardLight>
</Grid>}
<Grid item xs={12} md={5.5} lg={4} ><CardLight>
<Typography sx={{ textAlign: "center" }} py={1} variant="h6" color="text.secondary" >{t`editor.preview`}</Typography>
<ArtifactCard artifactObj={cachedArtifact} />
</CardLight></Grid>
</Grid>}
{/* Error alert */}
{!isValid && <Alert variant="filled" severity="error" >{errors.map((e, i) => <div key={i}>{e}</div>)}</Alert>}
{/* Buttons */}
<Grid container spacing={2}>
<Grid item>
{oldType === "edit" ?
<Button startIcon={<Add />} onClick={() => {
database.updateArt(editorArtifact!, old!.id);
if (allowEmpty) reset()
else {
setShow(false)
cancelEdit()
}
}} disabled={!editorArtifact || !isValid} color="primary">
{t`editor.btnSave`}
</Button> :
<Button startIcon={<Add />} onClick={() => {
database.createArt(artifact!);
if (allowEmpty) reset()
else {
setShow(false)
cancelEdit()
}
}} disabled={!artifact || !isValid} color={oldType === "duplicate" ? "warning" : "primary"}>
{t`editor.btnAdd`}
</Button>}
</Grid>
<Grid item flexGrow={1}>
{allowEmpty && <Button startIcon={<Replay />} disabled={!artifact} onClick={() => { canClearArtifact() && reset() }} color="error">{t`editor.btnClear`}</Button>}
</Grid>
<Grid item>
{process.env.NODE_ENV === "development" && <Button color="info" startIcon={<Shuffle />} onClick={async () => artifactDispatch({ type: "overwrite", artifact: await randomizeArtifact() })}>{t`editor.btnRandom`}</Button>}
</Grid>
{old && oldType !== "edit" && <Grid item>
<Button startIcon={<Update />} onClick={() => { database.updateArt(editorArtifact!, old.id); allowEmpty ? reset() : setShow(false) }} disabled={!editorArtifact || !isValid} color="success">{t`editor.btnUpdate`}</Button>
</Grid>}
</Grid>
</CardContent>
</CardDark ></Suspense>
</ModalWrapper>
}
Example #13
Source File: Scheduler.tsx From NekoMaid with MIT License | 4 votes |
Scheduler: React.FC = () => {
const plugin = usePlugin()
const [id, setId] = useState(-1)
let [tasks, setTasks] = useState<Task[]>([])
const [name, setName] = useState('')
const [cron, setCron] = useState('')
const [values, setValues] = useState('')
const [whenIdle, setWhenIdle] = useState(false)
const [cronError, setCronError] = useState('')
const save = () => plugin.emit('scheduler:update', (res: boolean) => {
action(res)
plugin.emit('scheduler:fetch', setTasks)
}, JSON.stringify(tasks))
useEffect(() => { plugin.emit('scheduler:fetch', setTasks) }, [])
return <Box sx={{ minHeight: '100%', py: 3 }}>
<Toolbar />
<Container maxWidth={false}>
<Grid container spacing={3}>
<Grid item lg={4} md={12} xl={4} xs={12}>
<Card>
<CardHeader
title={lang.scheduler.title}
sx={{ position: 'relative' }}
action={<IconButton
size='small'
onClick={() => {
const task = {
name: lang.scheduler.newTask,
cron: '*/1 * * * *',
enabled: true,
whenIdle: false,
values: ['/say Hello, %server_tps% (PlaceholderAPI)', 'This is a chat message']
}
setTasks([...tasks, task])
setId(tasks.length)
setCronError('')
setCron(task.cron)
setName(task.name)
setValues(task.values.join('\n'))
setWhenIdle(false)
}}
sx={cardActionStyles}
><Add /></IconButton>}
/>
<Divider />
{tasks.length
? <List
sx={{ width: '100%' }}
component='nav'
>
{tasks.map((it, i) => <ListItem
key={i}
disablePadding
secondaryAction={<IconButton
edge='end'
onClick={() => dialog(lang.scheduler.confirmDelete)
.then(it => {
if (it == null) return
setTasks((tasks = tasks.filter((_, id) => i !== id)))
save()
})}
><Delete /></IconButton>}
sx={{ position: 'relative' }}
>
<ListItemIcon sx={{ paddingLeft: 2, position: 'absolute' }}>
<Checkbox
edge='start'
checked={it.enabled}
tabIndex={-1}
/>
</ListItemIcon>
<ListItemButton onClick={() => {
setId(i)
setCronError('')
setCron(tasks[i].cron)
setName(tasks[i].name)
setValues(tasks[i].values.join('\n'))
setWhenIdle(!!tasks[i].whenIdle)
}}><ListItemText inset primary={it.name} /></ListItemButton >
</ListItem>)}
</List>
: <CardContent><Empty /></CardContent>}
</Card>
</Grid>
<Grid item lg={8} md={12} xl={8} xs={12}>
<Card>
<CardHeader
title={lang.scheduler.editor}
sx={{ position: 'relative' }}
action={<IconButton
size='small'
onClick={() => {
tasks[id].values = values.split('\n')
tasks[id].cron = cron
tasks[id].name = name
tasks[id].whenIdle = whenIdle
save()
}}
sx={cardActionStyles}
disabled={!tasks[id] || !!cronError}
><Save /></IconButton>}
/>
<Divider />
<CardContent>
{tasks[id]
? <>
<TextField
required
fullWidth
variant='standard'
label={lang.scheduler.name}
value={name}
onChange={e => setName(e.target.value)}
/>
<TextField
fullWidth
multiline
rows={4}
value={values}
sx={{ marginTop: 3 }}
label={lang.scheduler.content}
onChange={e => setValues(e.target.value)}
/>
<FormControlLabel
control={<Switch checked={whenIdle} />}
label={lang.scheduler.whenIdle}
onChange={(e: any) => setWhenIdle(e.target.checked)}
/>
</>
: <Empty title={lang.scheduler.notSelected} />}
</CardContent>
{tasks[id] && <>
<Divider textAlign='left'>{lang.scheduler.timer}</Divider>
<CardContent>
<Box sx={{
'& .MuiTextField-root': { backgroundColor: 'inherit!important' },
'& .MuiOutlinedInput-input': { color: 'inherit!important' },
'& .MuiTypography-h6': { color: theme => theme.palette.primary.main + '!important' }
}}>
<Cron cron={cron} setCron={setCron} setCronError={setCronError} locale={currentLanguage as any} isAdmin />
</Box>
</CardContent>
</>}
</Card>
</Grid>
</Grid>
</Container>
</Box>
}
Example #14
Source File: Chat.tsx From sapio-studio with Mozilla Public License 2.0 | 4 votes |
function Channels() {
const [channels, set_channels] = React.useState<{ channel_id: string }[]>(
[]
);
React.useEffect(() => {
let cancel: ReturnType<typeof window.setTimeout>;
async function f() {
set_channels(await window.electron.chat.list_channels());
cancel = setTimeout(f, 5000);
}
cancel = setTimeout(f, 0);
return () => {
clearTimeout(cancel);
};
}, []);
const [channel, set_channel] = React.useState<string | null>(null);
const [add_new_channel, set_add_new_channel] =
React.useState<boolean>(false);
const ChannelColumns: GridColumns = [
{
field: 'actions-load',
type: 'actions',
flex: 0.2,
getActions: (params) => [
<GridActionsCellItem
key="open-folder"
icon={<VisibilityIcon />}
label="Open"
onClick={() => {
typeof params.id === 'string' && set_channel(params.id);
}}
/>,
],
},
{
field: 'channel_id',
headerName: 'Channel',
minWidth: 100,
type: 'text',
flex: 1,
},
];
function CustomToolbar() {
return (
<GridToolbarContainer>
<Button onClick={() => set_add_new_channel(true)}>
Create New Channel<Add></Add>
</Button>
</GridToolbarContainer>
);
}
return (
<div>
<NewChannel
show={add_new_channel}
hide={() => set_add_new_channel(false)}
/>
{channel === null && (
<DataGrid
components={{ Toolbar: CustomToolbar }}
rows={channels.map((v) => {
return { id: v.channel_id, ...v };
})}
columns={ChannelColumns}
disableExtendRowFullWidth={false}
columnBuffer={3}
pageSize={10}
rowsPerPageOptions={[5]}
disableColumnSelector
disableSelectionOnClick
/>
)}
{channel !== null && (
<Channel
channel_id={channel}
close={() => {
set_channel(null);
}}
></Channel>
)}
</div>
);
}
Example #15
Source File: Workspaces.tsx From sapio-studio with Mozilla Public License 2.0 | 4 votes |
export function Workspaces(props: { idx: number; value: number }) {
const dispatch = useDispatch();
const [workspaces, set_workspaces] = React.useState<string[]>([]);
const [to_delete, set_to_delete] = React.useState<string | null>(null);
const [trigger_now, set_trigger_now] = React.useState(0);
const [show_new_workspace, set_new_workspace] = React.useState(false);
const hide_new_workspace = () => {
set_new_workspace(false);
};
const reload = () => {
set_trigger_now(trigger_now + 1);
};
React.useEffect(() => {
let cancel = false;
const update = async () => {
if (cancel) return;
try {
const list = await window.electron.sapio.workspaces.list();
set_workspaces(list);
} catch (err) {
console.error(err);
set_workspaces([]);
}
setTimeout(update, 5000);
};
update();
return () => {
cancel = true;
};
}, [trigger_now]);
const contract_rows = workspaces.map((id) => {
return {
id,
name: id,
};
});
const delete_workspace = (fname: string | number) => {
if (typeof fname === 'number') return;
set_to_delete(fname);
};
const columns: GridColumns = [
{
field: 'actions-load',
type: 'actions',
flex: 0.2,
getActions: (params) => [
<GridActionsCellItem
key="open-folder"
icon={<FolderOpen />}
label="Open"
onClick={() => {
// TODO: Better tabbing?
dispatch(switch_wallet_tab(3));
typeof params.id === 'string' &&
dispatch(switch_workspace(params.id));
}}
/>,
],
},
{
field: 'name',
headerName: 'Name',
width: 100,
type: 'text',
flex: 1,
},
{
field: 'actions-delete',
type: 'actions',
flex: 0.2,
getActions: (params) => [
<GridActionsCellItem
key="delete"
icon={<Delete />}
label="Delete"
onClick={() => delete_workspace(params.id)}
/>,
],
},
];
function CustomToolbar() {
return (
<GridToolbarContainer>
<Button onClick={() => set_new_workspace(true)}>
New Workspace<Add></Add>
</Button>
</GridToolbarContainer>
);
}
return (
<div hidden={props.idx !== props.value} className="WorkspaceList">
<NewWorkspace
show={show_new_workspace}
hide={hide_new_workspace}
reload={reload}
/>
<DeleteDialog
set_to_delete={() => set_to_delete(null)}
to_delete={to_delete !== null ? ['workspace', to_delete] : null}
reload={reload}
/>
{props.idx === props.value && (
<div className="WorkspaceListInner">
<div></div>
<div>
<DataGrid
components={{
Toolbar: CustomToolbar,
}}
rows={contract_rows}
columns={columns}
disableExtendRowFullWidth={false}
columnBuffer={3}
pageSize={10}
rowsPerPageOptions={[5]}
disableColumnSelector
disableSelectionOnClick
/>
</div>
<div></div>
</div>
)}
</div>
);
}
Example #16
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>
);
}