Files
dashfront/src/components/article/ArticleTileTab.jsx
seb 701487f525 Enlarge article tab icons and widen handle spacing.
Makes the icon-only tabs easier to hit and visually clearer.
2026-08-01 03:58:58 +02:00

295 lines
8.6 KiB
JavaScript

import { Component } from 'react';
import Box from '@mui/material/Box';
import Button from '@mui/material/Button';
import Tab from '@mui/material/Tab';
import Tabs from '@mui/material/Tabs';
import Typography from '@mui/material/Typography';
import AccountTreeIcon from '@mui/icons-material/AccountTree';
import CategoryIcon from '@mui/icons-material/Category';
import DeleteIcon from '@mui/icons-material/Delete';
import HistoryIcon from '@mui/icons-material/History';
import LabelIcon from '@mui/icons-material/Label';
import LocalShippingIcon from '@mui/icons-material/LocalShipping';
import ManageSearchIcon from '@mui/icons-material/ManageSearch';
import SettingsIcon from '@mui/icons-material/Settings';
import { article } from '../../api';
import { notifyError } from '../../utils/notify';
import { PanelTile } from '../panels';
import { PANEL_TILE_BORDER_RADIUS, responsiveColumnSpan } from '../panels/panelTileLayout';
const TAB = {
history: 'history',
supplier: 'supplier',
seo: 'seo',
label: 'label',
attributes: 'attributes',
compound: 'compound',
settings: 'settings',
};
const ALWAYS_TABS = [
{ id: TAB.history, label: 'History', color: '#fed7aa', Icon: HistoryIcon },
{ id: TAB.supplier, label: 'Supplier', color: '#bbf7d0', Icon: LocalShippingIcon },
{ id: TAB.seo, label: 'SEO & Beschreibung', color: '#99f6e4', Icon: ManageSearchIcon },
{ id: TAB.label, label: 'Label', color: '#fde68a', Icon: LabelIcon },
{ id: TAB.attributes, label: 'Attribute und Merkmale', color: '#bfdbfe', Icon: CategoryIcon },
];
const COMPOUND_TAB = {
id: TAB.compound,
label: 'Compound Editor',
color: '#e9d5ff',
Icon: AccountTreeIcon,
};
const SETTINGS_TAB = {
id: TAB.settings,
label: 'Einstellungen',
color: '#fca5a5',
Icon: SettingsIcon,
};
function isCompoundArticle(article) {
return (
article?.articleListCount > 0 ||
article?.optionsCount > 0 ||
article?.childCount > 0 ||
article?.fatherArticleId != null
);
}
function visibleTabs(article) {
const tabs = [...ALWAYS_TABS];
if (isCompoundArticle(article)) {
tabs.push(COMPOUND_TAB);
}
tabs.push(SETTINGS_TAB);
return tabs;
}
function PanelValue({ label, children }) {
if (children == null || children === '') return null;
return (
<Box sx={{ mb: 1 }}>
{label ? (
<Typography variant="caption" color="text.secondary" display="block">
{label}
</Typography>
) : null}
<Typography variant="body2" component="div">
{children}
</Typography>
</Box>
);
}
function Placeholder({ label }) {
return (
<Typography variant="body2" color="text.secondary">
{label} — noch nicht verfügbar.
</Typography>
);
}
class ArticleTileTab extends Component {
constructor(props) {
super(props);
this.state = { tab: TAB.seo, busy: false };
this.handleTabChange = this.handleTabChange.bind(this);
this.handleDeleteArticle = this.handleDeleteArticle.bind(this);
}
componentDidUpdate(prevProps) {
if (prevProps.article === this.props.article) return;
const tabs = visibleTabs(this.props.article);
if (!tabs.some((row) => row.id === this.state.tab)) {
this.setState({ tab: TAB.seo });
}
}
handleTabChange(_event, tab) {
this.setState({ tab });
}
async handleDeleteArticle() {
const { article: articleData, onArticleDeleted } = this.props;
if (this.state.busy) return;
if (!window.confirm(`Artikel „${articleData.name ?? articleData.sku}“ wirklich löschen?`)) {
return;
}
this.setState({ busy: true });
try {
await article.delete(articleData.sku);
onArticleDeleted?.(articleData);
} catch (err) {
notifyError(err);
this.setState({ busy: false });
}
}
renderSeo() {
const { article: articleData } = this.props;
return (
<>
<PanelValue label="Slug">{articleData.slug}</PanelValue>
<PanelValue label="SEO-Titel">{articleData.seoTitle}</PanelValue>
<PanelValue label="SEO-Beschreibung">{articleData.seoDescription}</PanelValue>
<PanelValue label="Beschreibung">{articleData.description}</PanelValue>
</>
);
}
renderSettings() {
const { busy } = this.state;
return (
<Box>
<Button
type="button"
color="error"
variant="contained"
startIcon={<DeleteIcon />}
disabled={busy}
onClick={() => void this.handleDeleteArticle()}
>
Artikel löschen
</Button>
</Box>
);
}
renderPanel() {
switch (this.state.tab) {
case TAB.history:
return <Placeholder label="History" />;
case TAB.supplier:
return <Placeholder label="Supplier" />;
case TAB.seo:
return this.renderSeo();
case TAB.label:
return <Placeholder label="Label" />;
case TAB.attributes:
return <Placeholder label="Attribute und Merkmale" />;
case TAB.compound:
return <Placeholder label="Compound Editor" />;
case TAB.settings:
return this.renderSettings();
default:
return null;
}
}
render() {
const { article: articleData, wide = 3, minHeight = 200, sx, tileKey } = this.props;
const tabs = visibleTabs(articleData);
const { tab } = this.state;
const active = tabs.find((row) => row.id === tab) ?? ALWAYS_TABS.find((row) => row.id === TAB.seo);
const activeColor = active?.color;
return (
<Box
sx={{
gridColumn: responsiveColumnSpan(wide),
display: 'flex',
flexDirection: 'column',
minWidth: 0,
filter:
'drop-shadow(0px 2px 4px rgba(0,0,0,0.2)) drop-shadow(0px 4px 5px rgba(0,0,0,0.14)) drop-shadow(0px 1px 10px rgba(0,0,0,0.12))',
}}
>
<Tabs
value={tab}
onChange={this.handleTabChange}
variant="standard"
TabIndicatorProps={{ sx: { display: 'none', height: 0 } }}
sx={{
position: 'relative',
zIndex: 1,
minHeight: 56,
px: 0,
mb: 0,
borderBottom: 'none',
overflow: 'visible',
'& .MuiTabs-scroller': {
px: 0,
borderBottom: 'none',
overflow: 'visible !important',
},
'& .MuiTabs-list, & .MuiTabs-flexContainer': {
flexWrap: 'wrap-reverse',
columnGap: 3,
rowGap: 0,
alignItems: 'stretch',
alignContent: 'flex-end',
},
'& .MuiTabs-indicator': { display: 'none' },
'& .MuiTab-root': {
flexShrink: 0,
maxWidth: 'none',
minHeight: 56,
marginBottom: 0,
},
}}
>
{tabs.map((row) => {
const selected = tab === row.id;
const { Icon } = row;
return (
<Tab
key={row.id}
value={row.id}
aria-label={row.label}
title={row.label}
icon={<Icon sx={{ fontSize: 40 }} />}
sx={{
minHeight: 56,
minWidth: 64,
py: 0.5,
px: 1.25,
border: 'none',
borderBottom: 'none',
borderRadius: '8px 8px 0 0',
textTransform: 'none',
bgcolor: row.color,
opacity: selected ? 1 : 0.72,
color: 'text.primary',
zIndex: selected ? 2 : 1,
boxShadow: 'none',
'&.Mui-selected': {
color: 'text.primary',
borderBottom: 'none',
},
}}
/>
);
})}
</Tabs>
<PanelTile
wide={wide}
minHeight={minHeight}
tileKey={tileKey}
elevation={0}
sx={{
gridColumn: 'auto',
width: '100%',
bgcolor: activeColor,
borderTopLeftRadius: 0,
borderTopRightRadius: (theme) =>
`${(theme.shape.borderRadius || 4) * PANEL_TILE_BORDER_RADIUS}px`,
borderBottomLeftRadius: (theme) =>
`${(theme.shape.borderRadius || 4) * PANEL_TILE_BORDER_RADIUS}px`,
borderBottomRightRadius: (theme) =>
`${(theme.shape.borderRadius || 4) * PANEL_TILE_BORDER_RADIUS}px`,
boxShadow: 'none',
...sx,
}}
>
{this.renderPanel()}
</PanelTile>
</Box>
);
}
}
export default ArticleTileTab;