first commit
Some checks failed
Build / run (push) Has been cancelled

This commit is contained in:
maher
2025-10-29 11:42:25 +01:00
commit 703f50a09d
4595 changed files with 385164 additions and 0 deletions

View File

@@ -0,0 +1,208 @@
import {message} from '@common/i18n/message';
import {BgSelectorTabProps} from '@common/background-selector/bg-selector-tab-props';
import {
BackgroundSelectorConfig,
EditableBackgroundProps,
} from '@common/background-selector/background-selector-config';
import {Trans} from '@common/i18n/trans';
import {RadioGroup} from '@common/ui/forms/radio-group/radio-group';
import {Radio} from '@common/ui/forms/radio-group/radio';
import {ButtonBase} from '@common/ui/buttons/button-base';
import clsx from 'clsx';
import {SegmentedRadio} from '@common/ui/forms/segmented-radio-group/segmented-radio';
import {SegmentedRadioGroup} from '@common/ui/forms/segmented-radio-group/segmented-radio-group';
const repeat = [
{
value: 'no-repeat',
label: message("Don't repeat"),
},
{
value: 'repeat-x',
label: message('Horizontal'),
},
{
value: 'repeat-y',
label: message('Vertical'),
},
{
value: 'repeat',
label: message('Both'),
},
];
const size = [
{
value: 'auto',
label: message('Auto'),
},
{
value: 'cover',
label: message('Stretch to fit'),
},
{
value: 'contain',
label: message('Fit image'),
},
];
const position = [
'left top',
'center top',
'right top',
'left center',
'center center',
'right center',
'left bottom',
'center bottom',
'right bottom',
];
export function AdvancedBackgroundPositionSelector({
value,
onChange,
}: BgSelectorTabProps<BackgroundSelectorConfig>) {
return (
<div className="mt-14 border-t pt-14">
<div className="flex gap-60">
<RepeatSelector value={value} onChange={onChange} />
<SizeSelector value={value} onChange={onChange} />
<PositionSelector value={value} onChange={onChange} />
</div>
<SegmentedRadioGroup
size="xs"
className="mt-20"
value={value?.backgroundAttachment ?? 'scroll'}
onChange={newValue => {
onChange?.({
...value!,
backgroundAttachment:
newValue as EditableBackgroundProps['backgroundAttachment'],
});
}}
>
<SegmentedRadio value="fixed">
<Trans message="Fixed" />
</SegmentedRadio>
<SegmentedRadio value="scroll">
<Trans message="Not fixed" />
</SegmentedRadio>
</SegmentedRadioGroup>
</div>
);
}
function RepeatSelector({
value,
onChange,
}: BgSelectorTabProps<BackgroundSelectorConfig>) {
return (
<div>
<div className="mb-10">
<Trans message="Repeat" />
</div>
<RadioGroup orientation="vertical" size="sm" disabled={!value}>
{repeat.map(({value: repeatValue, label}) => (
<Radio
key={repeatValue}
value={repeatValue}
checked={value?.backgroundRepeat === repeatValue}
onChange={() => {
onChange?.({
...value!,
backgroundRepeat:
repeatValue as EditableBackgroundProps['backgroundRepeat'],
});
}}
>
<Trans {...label} />
</Radio>
))}
</RadioGroup>
</div>
);
}
function SizeSelector({
value,
onChange,
}: BgSelectorTabProps<BackgroundSelectorConfig>) {
return (
<div>
<div className="mb-10">
<Trans message="Size" />
</div>
<RadioGroup orientation="vertical" size="sm" disabled={!value}>
{size.map(({value: sizeValue, label}) => (
<Radio
key={sizeValue}
value={sizeValue}
checked={value?.backgroundSize === sizeValue}
onChange={() => {
onChange?.({
...value!,
backgroundSize:
sizeValue as EditableBackgroundProps['backgroundSize'],
});
}}
>
<Trans {...label} />
</Radio>
))}
</RadioGroup>
</div>
);
}
function PositionSelector({
value,
onChange,
}: BgSelectorTabProps<BackgroundSelectorConfig>) {
return (
<div>
<div className="mb-10">
<Trans message="Position" />
</div>
<div className="grid grid-cols-3 gap-8">
{position.map(position => (
<PositionSelectorButton
disabled={!value}
key={position}
value={value}
onChange={onChange}
position={position}
/>
))}
</div>
</div>
);
}
interface PositionSelectorButtonProps {
value: BackgroundSelectorConfig | undefined;
onChange: (value: BackgroundSelectorConfig) => void;
position: string;
disabled: boolean;
}
function PositionSelectorButton({
value,
onChange,
position,
disabled,
}: PositionSelectorButtonProps) {
return (
<ButtonBase
disabled={disabled}
onClick={() => {
onChange({
...value!,
backgroundPosition: position,
});
}}
className={clsx(
'h-26 w-26 rounded border',
value?.backgroundPosition === position ? 'bg-primary' : 'bg-alt',
)}
/>
);
}

View File

@@ -0,0 +1,171 @@
import {DialogTrigger} from '@common/ui/overlays/dialog/dialog-trigger';
import {Trans} from '@common/i18n/trans';
import {UploadIcon} from '@common/icons/material/Upload';
import {useForm} from 'react-hook-form';
import {useDialogContext} from '@common/ui/overlays/dialog/dialog-context';
import {Dialog} from '@common/ui/overlays/dialog/dialog';
import {DialogHeader} from '@common/ui/overlays/dialog/dialog-header';
import {DialogBody} from '@common/ui/overlays/dialog/dialog-body';
import {Form} from '@common/ui/forms/form';
import {FileUploadProvider} from '@common/uploads/uploader/file-upload-provider';
import {FormImageSelector} from '@common/ui/images/image-selector';
import {DialogFooter} from '@common/ui/overlays/dialog/dialog-footer';
import {Button} from '@common/ui/buttons/button';
import {
BaseImageBg,
ImageBackgrounds,
} from '@common/background-selector/image-backgrounds';
import {BackgroundSelectorButton} from '@common/background-selector/background-selector-button';
import {cssPropsFromBgConfig} from '@common/background-selector/css-props-from-bg-config';
import {SimpleBackgroundPositionSelector} from '@common/background-selector/image-background-tab/simple-background-position-selector';
import {BgSelectorTabProps} from '@common/background-selector/bg-selector-tab-props';
import {BackgroundSelectorConfig} from '@common/background-selector/background-selector-config';
import {AdvancedBackgroundPositionSelector} from '@common/background-selector/image-background-tab/advanced-background-position-selector';
import {urlFromBackgroundImage} from '@common/background-selector/bg-config-from-css-props';
export function ImageBackgroundTab({
value,
onChange,
className,
positionSelector,
diskPrefix,
isInsideDialog,
}: BgSelectorTabProps<BackgroundSelectorConfig>) {
return (
<div>
<div className={className}>
<CustomImageTrigger
value={value}
onChange={onChange}
diskPrefix={diskPrefix}
hideFooter={isInsideDialog}
/>
{ImageBackgrounds.map(background => (
<BackgroundSelectorButton
key={background.id}
onClick={() =>
onChange?.({
...BaseImageBg,
...background,
})
}
isActive={value?.id === background.id}
style={{
...cssPropsFromBgConfig(background),
backgroundAttachment: 'initial',
}}
label={<Trans {...background.label} />}
/>
))}
</div>
{positionSelector === 'advanced' ? (
<AdvancedBackgroundPositionSelector value={value} onChange={onChange} />
) : (
<SimpleBackgroundPositionSelector value={value} onChange={onChange} />
)}
</div>
);
}
interface CustomImageTrigger {
value?: BackgroundSelectorConfig;
onChange?: (value: BackgroundSelectorConfig | null) => void;
diskPrefix?: string;
hideFooter?: boolean;
}
function CustomImageTrigger({
value,
onChange,
diskPrefix,
hideFooter,
}: CustomImageTrigger) {
// only seed form with custom uploaded image
value = value?.id === BaseImageBg.id ? value : undefined;
return (
<DialogTrigger
type="popover"
onClose={(imageUrl?: string) => {
onChange?.(
imageUrl
? {
...BaseImageBg,
backgroundImage: `url(${imageUrl})`,
}
: null,
);
}}
>
<BackgroundSelectorButton
label={<Trans {...BaseImageBg.label} />}
isActive={
value?.id === BaseImageBg.id && value?.backgroundImage !== 'none'
}
className="border-2 border-dashed"
style={cssPropsFromBgConfig(value)}
>
<span className="inline-block rounded bg-black/20 p-10 text-white">
<UploadIcon size="lg" />
</span>
</BackgroundSelectorButton>
<CustomImageDialog
value={value}
diskPrefix={diskPrefix}
hideFooter={hideFooter}
/>
</DialogTrigger>
);
}
interface CustomImageDialogProps {
value?: BackgroundSelectorConfig;
diskPrefix?: string;
hideFooter?: boolean;
}
export function CustomImageDialog({
value,
diskPrefix,
hideFooter,
}: CustomImageDialogProps) {
const defaultValue =
!value?.backgroundImage || !value.backgroundImage.includes('url(')
? undefined
: urlFromBackgroundImage(value.backgroundImage);
const form = useForm<{imageUrl: string}>({
defaultValues: {imageUrl: defaultValue},
});
const {close, formId} = useDialogContext();
return (
<Dialog size="sm">
<DialogHeader>
<Trans message="Upload image" />
</DialogHeader>
<DialogBody>
<Form
id={formId}
form={form}
onSubmit={values => close(values.imageUrl)}
>
<FileUploadProvider>
<FormImageSelector
autoFocus
name="imageUrl"
diskPrefix={diskPrefix || 'biolinks'}
showRemoveButton
onChange={hideFooter ? imageUrl => close(imageUrl) : undefined}
/>
</FileUploadProvider>
</Form>
</DialogBody>
{!hideFooter && (
<DialogFooter>
<Button onClick={() => close()}>
<Trans message="Cancel" />
</Button>
<Button variant="flat" color="primary" type="submit" form={formId}>
<Trans message="Select" />
</Button>
</DialogFooter>
)}
</Dialog>
);
}

View File

@@ -0,0 +1,83 @@
import {RadioGroup} from '@common/ui/forms/radio-group/radio-group';
import {Radio} from '@common/ui/forms/radio-group/radio';
import {Trans} from '@common/i18n/trans';
import {MessageDescriptor} from '@common/i18n/message-descriptor';
import {message} from '@common/i18n/message';
import {BgSelectorTabProps} from '@common/background-selector/bg-selector-tab-props';
import {BackgroundSelectorConfig} from '@common/background-selector/background-selector-config';
const BackgroundPositions: Record<
'cover' | 'contain' | 'repeat',
{
label: MessageDescriptor;
bgConfig: Partial<BackgroundSelectorConfig>;
}
> = {
cover: {
label: message('Stretch to fit'),
bgConfig: {
backgroundRepeat: 'no-repeat',
backgroundSize: 'cover',
},
},
contain: {
label: message('Fit image'),
bgConfig: {
backgroundRepeat: 'no-repeat',
backgroundSize: 'contain',
backgroundPosition: 'center top',
},
},
repeat: {
label: message('Repeat image'),
bgConfig: {
backgroundRepeat: 'repeat',
backgroundSize: undefined,
backgroundPosition: 'left top',
},
},
};
export function SimpleBackgroundPositionSelector({
value: imageBgValue,
onChange,
}: BgSelectorTabProps<BackgroundSelectorConfig>) {
const selectedPosition = positionKeyFromValue(imageBgValue);
return (
<div className="mt-20 border-t pt-14">
<RadioGroup size="sm" disabled={!imageBgValue}>
{Object.entries(BackgroundPositions).map(([key, position]) => (
<Radio
key={key}
name="background-position"
value={key}
checked={key === selectedPosition}
onChange={e => {
if (imageBgValue) {
onChange?.({
...imageBgValue,
...position.bgConfig,
});
}
}}
>
<Trans {...position.label} />
</Radio>
))}
</RadioGroup>
</div>
);
}
function positionKeyFromValue(
value?: BackgroundSelectorConfig,
): keyof typeof BackgroundPositions {
if (value?.backgroundSize === 'cover') {
return 'cover';
} else if (value?.backgroundSize === 'contain') {
return 'contain';
} else {
return 'repeat';
}
}