[Glitch] Profile editing: Edit image menu
Port 420136e83b65a467a62da739a679ccb579525e63 to glitch-soc Co-authored-by: diondiondion <mail@diondiondion.com> Signed-off-by: Claire <claire.github-309c@sitedethib.com>
This commit is contained in:
parent
ba7db10fd0
commit
a7974df51a
@ -296,6 +296,7 @@ interface DropdownProps<Item extends object | null = MenuItem> {
|
|||||||
children?: React.ReactElement;
|
children?: React.ReactElement;
|
||||||
icon?: string;
|
icon?: string;
|
||||||
iconComponent?: IconProp;
|
iconComponent?: IconProp;
|
||||||
|
iconClassName?: string;
|
||||||
items?: Item[];
|
items?: Item[];
|
||||||
loading?: boolean;
|
loading?: boolean;
|
||||||
title?: string;
|
title?: string;
|
||||||
@ -326,6 +327,7 @@ export const Dropdown = <Item extends object | null = MenuItem>({
|
|||||||
children,
|
children,
|
||||||
icon,
|
icon,
|
||||||
iconComponent,
|
iconComponent,
|
||||||
|
iconClassName,
|
||||||
items,
|
items,
|
||||||
loading,
|
loading,
|
||||||
title = 'Menu',
|
title = 'Menu',
|
||||||
@ -499,6 +501,7 @@ export const Dropdown = <Item extends object | null = MenuItem>({
|
|||||||
iconComponent={iconComponent}
|
iconComponent={iconComponent}
|
||||||
title={title}
|
title={title}
|
||||||
active={open}
|
active={open}
|
||||||
|
className={iconClassName}
|
||||||
{...buttonProps}
|
{...buttonProps}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -0,0 +1,154 @@
|
|||||||
|
import { useCallback, useMemo } from 'react';
|
||||||
|
import type { FC } from 'react';
|
||||||
|
|
||||||
|
import { defineMessages, useIntl } from 'react-intl';
|
||||||
|
|
||||||
|
import classNames from 'classnames';
|
||||||
|
|
||||||
|
import type { OffsetValue } from 'react-overlays/esm/usePopper';
|
||||||
|
|
||||||
|
import type { ModalType } from '@/flavours/glitch/actions/modal';
|
||||||
|
import { openModal } from '@/flavours/glitch/actions/modal';
|
||||||
|
import { Dropdown } from '@/flavours/glitch/components/dropdown_menu';
|
||||||
|
import { IconButton } from '@/flavours/glitch/components/icon_button';
|
||||||
|
import type { MenuItem } from '@/flavours/glitch/models/dropdown_menu';
|
||||||
|
import {
|
||||||
|
createAppSelector,
|
||||||
|
useAppDispatch,
|
||||||
|
useAppSelector,
|
||||||
|
} from '@/flavours/glitch/store';
|
||||||
|
import AddIcon from '@/material-icons/400-24px/add.svg?react';
|
||||||
|
import DeleteIcon from '@/material-icons/400-24px/delete.svg?react';
|
||||||
|
import EditIcon from '@/material-icons/400-24px/edit.svg?react';
|
||||||
|
import CameraIcon from '@/material-icons/400-24px/photo_camera.svg?react';
|
||||||
|
import ReplaceImageIcon from '@/material-icons/400-24px/replace_image.svg?react';
|
||||||
|
|
||||||
|
import classes from '../styles.module.scss';
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
add: {
|
||||||
|
id: 'account_edit.image_edit.add_button',
|
||||||
|
defaultMessage: 'Add image',
|
||||||
|
},
|
||||||
|
replace: {
|
||||||
|
id: 'account_edit.image_edit.replace_button',
|
||||||
|
defaultMessage: 'Replace image',
|
||||||
|
},
|
||||||
|
altAdd: {
|
||||||
|
id: 'account_edit.image_edit.alt_add_button',
|
||||||
|
description: 'Alt is short for "alternative".',
|
||||||
|
defaultMessage: 'Add alt text',
|
||||||
|
},
|
||||||
|
altEdit: {
|
||||||
|
id: 'account_edit.image_edit.alt_edit_button',
|
||||||
|
description: 'Alt is short for "alternative".',
|
||||||
|
defaultMessage: 'Edit alt text',
|
||||||
|
},
|
||||||
|
remove: {
|
||||||
|
id: 'account_edit.image_edit.remove_button',
|
||||||
|
defaultMessage: 'Remove image',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export type ImageLocation = 'avatar' | 'header';
|
||||||
|
|
||||||
|
const selectImageInfo = createAppSelector(
|
||||||
|
[
|
||||||
|
(state) => state.profileEdit.profile,
|
||||||
|
(_, location: ImageLocation) => location,
|
||||||
|
],
|
||||||
|
(profile, location) => {
|
||||||
|
if (!profile) {
|
||||||
|
return {
|
||||||
|
hasImage: false,
|
||||||
|
hasAlt: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
hasImage: !!profile[`${location}Static`],
|
||||||
|
hasAlt: !!profile[`${location}Description`],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
export const AccountImageEdit: FC<{
|
||||||
|
className?: string;
|
||||||
|
location: ImageLocation;
|
||||||
|
}> = ({ className, location }) => {
|
||||||
|
const intl = useIntl();
|
||||||
|
const { hasAlt, hasImage } = useAppSelector((state) =>
|
||||||
|
selectImageInfo(state, location),
|
||||||
|
);
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
|
const handleModal = useCallback(
|
||||||
|
(type: ModalType) => {
|
||||||
|
dispatch(openModal({ modalType: type, modalProps: { location } }));
|
||||||
|
},
|
||||||
|
[dispatch, location],
|
||||||
|
);
|
||||||
|
|
||||||
|
const items = useMemo(
|
||||||
|
() =>
|
||||||
|
[
|
||||||
|
{
|
||||||
|
text: intl.formatMessage(messages.replace),
|
||||||
|
action: () => {
|
||||||
|
handleModal('ACCOUNT_EDIT_IMAGE_UPLOAD');
|
||||||
|
},
|
||||||
|
icon: ReplaceImageIcon,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: intl.formatMessage(hasAlt ? messages.altEdit : messages.altAdd),
|
||||||
|
action: () => {
|
||||||
|
handleModal('ACCOUNT_EDIT_IMAGE_ALT');
|
||||||
|
},
|
||||||
|
icon: hasAlt ? EditIcon : AddIcon,
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
{
|
||||||
|
text: intl.formatMessage(messages.remove),
|
||||||
|
action: () => {
|
||||||
|
handleModal('ACCOUNT_EDIT_IMAGE_DELETE');
|
||||||
|
},
|
||||||
|
icon: DeleteIcon,
|
||||||
|
dangerous: true,
|
||||||
|
},
|
||||||
|
] satisfies MenuItem[],
|
||||||
|
[handleModal, hasAlt, intl],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleAddImage = useCallback(() => {
|
||||||
|
handleModal('ACCOUNT_EDIT_IMAGE_UPLOAD');
|
||||||
|
}, [handleModal]);
|
||||||
|
|
||||||
|
const iconClassName = classNames(classes.imageButton, className);
|
||||||
|
|
||||||
|
if (!hasImage) {
|
||||||
|
return (
|
||||||
|
<IconButton
|
||||||
|
title={intl.formatMessage(messages.add)}
|
||||||
|
icon='camera'
|
||||||
|
iconComponent={CameraIcon}
|
||||||
|
className={iconClassName}
|
||||||
|
onClick={handleAddImage}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dropdown
|
||||||
|
items={items}
|
||||||
|
placement={location === 'header' ? 'bottom-end' : 'bottom-start'}
|
||||||
|
offset={popperOffset}
|
||||||
|
className={classes.imageMenu}
|
||||||
|
icon='camera'
|
||||||
|
title={intl.formatMessage(messages.replace)}
|
||||||
|
iconComponent={CameraIcon}
|
||||||
|
iconClassName={iconClassName}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const popperOffset = [0, 6] as OffsetValue;
|
||||||
@ -23,6 +23,7 @@ import { AccountEditColumn, AccountEditEmptyColumn } from './components/column';
|
|||||||
import { EditButton } from './components/edit_button';
|
import { EditButton } from './components/edit_button';
|
||||||
import { AccountField } from './components/field';
|
import { AccountField } from './components/field';
|
||||||
import { AccountFieldActions } from './components/field_actions';
|
import { AccountFieldActions } from './components/field_actions';
|
||||||
|
import { AccountImageEdit } from './components/image_edit';
|
||||||
import { AccountEditSection } from './components/section';
|
import { AccountEditSection } from './components/section';
|
||||||
import classes from './styles.module.scss';
|
import classes from './styles.module.scss';
|
||||||
|
|
||||||
@ -164,8 +165,12 @@ export const AccountEdit: FC = () => {
|
|||||||
<header>
|
<header>
|
||||||
<div className={classes.profileImage}>
|
<div className={classes.profileImage}>
|
||||||
{headerSrc && <img src={headerSrc} alt='' />}
|
{headerSrc && <img src={headerSrc} alt='' />}
|
||||||
|
<AccountImageEdit location='header' />
|
||||||
|
</div>
|
||||||
|
<div className={classes.avatar}>
|
||||||
|
<Avatar account={account} size={80} />
|
||||||
|
<AccountImageEdit location='avatar' />
|
||||||
</div>
|
</div>
|
||||||
<Avatar account={account} size={80} className={classes.avatar} />
|
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<CustomEmojiProvider emojis={emojis}>
|
<CustomEmojiProvider emojis={emojis}>
|
||||||
|
|||||||
@ -0,0 +1,11 @@
|
|||||||
|
import type { FC } from 'react';
|
||||||
|
|
||||||
|
import { DialogModal } from '../../ui/components/dialog_modal';
|
||||||
|
import type { DialogModalProps } from '../../ui/components/dialog_modal';
|
||||||
|
import type { ImageLocation } from '../components/image_edit';
|
||||||
|
|
||||||
|
export const ImageAltModal: FC<
|
||||||
|
DialogModalProps & { location: ImageLocation }
|
||||||
|
> = ({ onClose }) => {
|
||||||
|
return <DialogModal title='TODO' onClose={onClose} />;
|
||||||
|
};
|
||||||
@ -0,0 +1,11 @@
|
|||||||
|
import type { FC } from 'react';
|
||||||
|
|
||||||
|
import { DialogModal } from '../../ui/components/dialog_modal';
|
||||||
|
import type { DialogModalProps } from '../../ui/components/dialog_modal';
|
||||||
|
import type { ImageLocation } from '../components/image_edit';
|
||||||
|
|
||||||
|
export const ImageDeleteModal: FC<
|
||||||
|
DialogModalProps & { location: ImageLocation }
|
||||||
|
> = ({ onClose }) => {
|
||||||
|
return <DialogModal title='TODO' onClose={onClose} />;
|
||||||
|
};
|
||||||
@ -0,0 +1,11 @@
|
|||||||
|
import type { FC } from 'react';
|
||||||
|
|
||||||
|
import { DialogModal } from '../../ui/components/dialog_modal';
|
||||||
|
import type { DialogModalProps } from '../../ui/components/dialog_modal';
|
||||||
|
import type { ImageLocation } from '../components/image_edit';
|
||||||
|
|
||||||
|
export const ImageUploadModal: FC<
|
||||||
|
DialogModalProps & { location: ImageLocation }
|
||||||
|
> = ({ onClose }) => {
|
||||||
|
return <DialogModal title='TODO' onClose={onClose} />;
|
||||||
|
};
|
||||||
@ -1,6 +1,9 @@
|
|||||||
export * from './bio_modal';
|
export * from './bio_modal';
|
||||||
export * from './fields_modals';
|
export * from './fields_modals';
|
||||||
export * from './fields_reorder_modal';
|
export * from './fields_reorder_modal';
|
||||||
|
export * from './image_alt';
|
||||||
|
export * from './image_delete';
|
||||||
|
export * from './image_upload';
|
||||||
export * from './name_modal';
|
export * from './name_modal';
|
||||||
export * from './profile_display_modal';
|
export * from './profile_display_modal';
|
||||||
export * from './verified_modal';
|
export * from './verified_modal';
|
||||||
|
|||||||
@ -5,6 +5,7 @@
|
|||||||
background: var(--color-bg-secondary);
|
background: var(--color-bg-secondary);
|
||||||
border-bottom: 1px solid var(--color-border-primary);
|
border-bottom: 1px solid var(--color-border-primary);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
@container (width >= 500px) {
|
@container (width >= 500px) {
|
||||||
height: 160px;
|
height: 160px;
|
||||||
@ -16,12 +17,27 @@
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.imageButton {
|
||||||
|
top: 16px;
|
||||||
|
right: 24px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.avatar {
|
.avatar {
|
||||||
margin-top: -64px;
|
margin-top: -64px;
|
||||||
margin-left: 18px;
|
margin-left: 18px;
|
||||||
border: 1px solid var(--color-border-primary);
|
position: relative;
|
||||||
|
width: 82px;
|
||||||
|
|
||||||
|
> :global(.account__avatar) {
|
||||||
|
border: 1px solid var(--color-border-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.imageButton {
|
||||||
|
bottom: -8px;
|
||||||
|
right: -8px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.field {
|
.field {
|
||||||
@ -153,6 +169,41 @@
|
|||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Image edit component
|
||||||
|
|
||||||
|
.imageButton {
|
||||||
|
--default-bg-color: var(--color-bg-primary);
|
||||||
|
|
||||||
|
&,
|
||||||
|
&:global(.active) {
|
||||||
|
// Overrides the transparent background added by default with .active
|
||||||
|
--hover-bg-color: var(--color-bg-brand-softer-solid);
|
||||||
|
}
|
||||||
|
|
||||||
|
position: absolute;
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
border: 1px solid var(--color-border-primary);
|
||||||
|
border-radius: 9999px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
padding: 4px;
|
||||||
|
transition:
|
||||||
|
color 0.2s ease-in-out,
|
||||||
|
background-color 0.2s ease-in-out;
|
||||||
|
|
||||||
|
svg {
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.imageMenu {
|
||||||
|
svg {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Item list component
|
// Item list component
|
||||||
|
|
||||||
.itemList {
|
.itemList {
|
||||||
|
|||||||
@ -293,11 +293,7 @@ svg.badgeIcon {
|
|||||||
|
|
||||||
.fieldOverflowButton {
|
.fieldOverflowButton {
|
||||||
--default-bg-color: var(--color-bg-secondary-solid);
|
--default-bg-color: var(--color-bg-secondary-solid);
|
||||||
--hover-bg-color: color-mix(
|
--hover-bg-color: var(--color-bg-brand-softer-solid);
|
||||||
in oklab,
|
|
||||||
var(--color-bg-brand-base),
|
|
||||||
var(--default-bg-color) var(--overlay-strength-brand)
|
|
||||||
);
|
|
||||||
|
|
||||||
position: absolute;
|
position: absolute;
|
||||||
right: 8px;
|
right: 8px;
|
||||||
|
|||||||
@ -110,6 +110,9 @@ export const MODAL_COMPONENTS = {
|
|||||||
'ACCOUNT_EDIT_FIELD_EDIT': accountEditModal('EditFieldModal'),
|
'ACCOUNT_EDIT_FIELD_EDIT': accountEditModal('EditFieldModal'),
|
||||||
'ACCOUNT_EDIT_FIELD_DELETE': accountEditModal('DeleteFieldModal'),
|
'ACCOUNT_EDIT_FIELD_DELETE': accountEditModal('DeleteFieldModal'),
|
||||||
'ACCOUNT_EDIT_FIELDS_REORDER': accountEditModal('ReorderFieldsModal'),
|
'ACCOUNT_EDIT_FIELDS_REORDER': accountEditModal('ReorderFieldsModal'),
|
||||||
|
'ACCOUNT_EDIT_IMAGE_ALT': accountEditModal('ImageAltModal'),
|
||||||
|
'ACCOUNT_EDIT_IMAGE_DELETE': accountEditModal('ImageDeleteModal'),
|
||||||
|
'ACCOUNT_EDIT_IMAGE_UPLOAD': accountEditModal('ImageUploadModal'),
|
||||||
};
|
};
|
||||||
|
|
||||||
/** @arg {keyof import('@/flavours/glitch/features/account_edit/modals')} type */
|
/** @arg {keyof import('@/flavours/glitch/features/account_edit/modals')} type */
|
||||||
|
|||||||
@ -81,6 +81,11 @@
|
|||||||
var(--color-bg-brand-base),
|
var(--color-bg-brand-base),
|
||||||
var(--overlay-strength-brand)
|
var(--overlay-strength-brand)
|
||||||
)};
|
)};
|
||||||
|
--color-bg-brand-softer-solid: color-mix(
|
||||||
|
in srgb,
|
||||||
|
var(--color-bg-primary),
|
||||||
|
var(--color-bg-brand-base) var(--overlay-strength-brand)
|
||||||
|
);
|
||||||
|
|
||||||
// Error
|
// Error
|
||||||
--overlay-strength-error: 10%;
|
--overlay-strength-error: 10%;
|
||||||
|
|||||||
@ -78,6 +78,11 @@
|
|||||||
#0012d8,
|
#0012d8,
|
||||||
var(--overlay-strength-brand)
|
var(--overlay-strength-brand)
|
||||||
)};
|
)};
|
||||||
|
--color-bg-brand-softer-solid: color-mix(
|
||||||
|
in srgb,
|
||||||
|
var(--color-bg-primary),
|
||||||
|
var(--color-bg-brand-base) var(--overlay-strength-brand)
|
||||||
|
);
|
||||||
|
|
||||||
// Error
|
// Error
|
||||||
--overlay-strength-error: 5%;
|
--overlay-strength-error: 5%;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user