Files
emaui/apps/backoffice/src/app/features/certificate-designer/components/BlockPropertiesPanel.tsx

238 lines
7.3 KiB
TypeScript

import {
ActionIcon,
Button,
ColorInput,
Group,
NumberInput,
Paper,
SegmentedControl,
Select,
Stack,
Text,
TextInput,
Tooltip,
} from '@mantine/core';
import { IconBold, IconItalic, IconTrash } from '@tabler/icons-react';
import { useTranslation } from 'react-i18next';
import type { TemplateFieldPlacement, TemplateVariable } from '@ema-platform/api';
interface Props {
block: TemplateFieldPlacement | null;
variables: TemplateVariable[];
onChange: (block: TemplateFieldPlacement) => void;
onDelete: (id: string) => void;
disabled: boolean;
}
/** Everything about the selected block that is not its position on the page. */
export function BlockPropertiesPanel({
block,
variables,
onChange,
onDelete,
disabled,
}: Props) {
const { t } = useTranslation();
if (!block) {
return (
<Paper withBorder p="md" radius="md">
<Text size="sm" c="dimmed">
{t('designer.noBlockSelected', 'Select a block on the page to edit it.')}
</Text>
</Paper>
);
}
const current = block;
const isLiteral = current.variable === null;
const isImage = current.type === 'image';
const variableByKey = new Map(variables.map((v) => [v.key, v]));
const selectVariable = (key: string | null) => {
if (!key) return;
const kind: 'text' | 'image' =
variableByKey.get(key)?.kind === 'image' ? 'image' : 'text';
onChange({ ...current, variable: key, type: kind });
};
return (
<Paper withBorder p="md" radius="md">
<Stack gap="sm">
<Group justify="space-between" align="center">
<Text fw={600} size="sm">
{t('designer.blockProperties', 'Selected block')}
</Text>
<Tooltip label={t('designer.deleteBlock', 'Remove block')}>
<ActionIcon
variant="subtle"
color="red"
disabled={disabled}
onClick={() => onDelete(block.id)}
>
<IconTrash size={16} />
</ActionIcon>
</Tooltip>
</Group>
<SegmentedControl
fullWidth
size="xs"
value={isLiteral ? 'text' : 'variable'}
disabled={disabled}
onChange={(value) =>
value === 'text'
? onChange({ ...block, variable: null, type: 'text', text: block.text ?? '' })
: selectVariable(variables[0]?.key ?? 'companyName')
}
data={[
{ value: 'variable', label: t('designer.blockVariable', 'Variable') },
{ value: 'text', label: t('designer.blockText', 'Fixed text') },
]}
/>
{isLiteral ? (
<TextInput
label={t('designer.blockTextLabel', 'Text')}
value={block.text ?? ''}
onChange={(e) => onChange({ ...block, text: e.currentTarget.value })}
disabled={disabled}
/>
) : (
<Select
label={t('designer.blockVariableLabel', 'Variable')}
data={variables.map((variable) => ({
value: variable.key,
label: variable.kind === 'image' ? `🖼 ${variable.label}` : variable.label,
}))}
value={block.variable}
onChange={selectVariable}
searchable
disabled={disabled}
/>
)}
<Group gap="xs" grow>
{!isImage && (
<NumberInput
label={t('designer.blockFontSize', 'Font size')}
value={block.fontSize ?? 14}
onChange={(value) => onChange({ ...block, fontSize: Number(value) || 14 })}
min={4}
max={200}
disabled={disabled}
/>
)}
<NumberInput
label={t('designer.blockWidth', 'Width (%)')}
value={block.widthPct}
onChange={(value) =>
onChange({ ...block, widthPct: Math.min(100, Math.max(1, Number(value) || 1)) })
}
min={1}
max={100}
disabled={disabled}
/>
</Group>
<NumberInput
label={t('designer.blockPage', 'Page')}
description={t('designer.blockPageHint', 'Which sheet of the document this block prints on')}
value={(block.page ?? 0) + 1}
onChange={(value) => {
const index = Math.max(0, Math.min(63, Math.floor(Number(value) || 1) - 1));
// Omitted on the first page so a single-page design stays as it was.
const { page: _page, ...rest } = block;
onChange(index > 0 ? { ...rest, page: index } : rest);
}}
min={1}
max={64}
disabled={disabled}
/>
<Group gap="xs" grow>
<NumberInput
label={t('designer.blockX', 'X (%)')}
value={block.xPct}
onChange={(value) =>
onChange({ ...block, xPct: Math.min(100, Math.max(0, Number(value) || 0)) })
}
min={0}
max={100}
decimalScale={2}
disabled={disabled}
/>
<NumberInput
label={t('designer.blockY', 'Y (%)')}
value={block.yPct}
onChange={(value) =>
onChange({ ...block, yPct: Math.min(100, Math.max(0, Number(value) || 0)) })
}
min={0}
max={100}
decimalScale={2}
disabled={disabled}
/>
</Group>
{!isImage && (
<>
<SegmentedControl
fullWidth
size="xs"
value={block.align ?? 'left'}
disabled={disabled}
onChange={(value) =>
onChange({ ...block, align: value as TemplateFieldPlacement['align'] })
}
data={[
{ value: 'left', label: t('designer.alignLeft', 'Left') },
{ value: 'center', label: t('designer.alignCenter', 'Centre') },
{ value: 'right', label: t('designer.alignRight', 'Right') },
{ value: 'justify', label: t('designer.alignJustify', 'Justify') },
]}
/>
<Group gap="xs">
<Button
size="xs"
variant={block.fontWeight === 'bold' ? 'filled' : 'default'}
disabled={disabled}
onClick={() =>
onChange({
...block,
fontWeight: block.fontWeight === 'bold' ? 'normal' : 'bold',
})
}
>
<IconBold size={14} />
</Button>
<Button
size="xs"
variant={block.fontStyle === 'italic' ? 'filled' : 'default'}
disabled={disabled}
onClick={() =>
onChange({
...block,
fontStyle: block.fontStyle === 'italic' ? 'normal' : 'italic',
})
}
>
<IconItalic size={14} />
</Button>
</Group>
<ColorInput
label={t('designer.blockColor', 'Colour')}
value={block.color ?? '#111111'}
onChange={(value) => onChange({ ...block, color: value })}
disabled={disabled}
format="hex"
/>
</>
)}
</Stack>
</Paper>
);
}