Skip to content

Commit

Permalink
Edit profile (#384)
Browse files Browse the repository at this point in the history
* feat(): edit profile WIP

* fix(): avatar fix and refactor file structure

* fix(): ref types, file paths

* fix():  file paths

* refactor(): split profile card editable fields into multiple controlled components

* fix(): theme type, hide edit buttons when missing data, replace px with em

* fix(): replace rest of px with em
  • Loading branch information
quininez committed Jan 29, 2020
1 parent 0109b10 commit 8dbe6a8
Show file tree
Hide file tree
Showing 104 changed files with 1,785 additions and 1,056 deletions.
13 changes: 8 additions & 5 deletions ui/design/src/components/Avatar/__test__/index.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ describe('<Avatar /> component', () => {
let componentWrapper: ReactTestRenderer = create(<></>);
beforeEach(() => {
act(() => {
componentWrapper = create(wrapWithTheme(<Avatar guest={true} src={'0x01230123450012312'} />));
componentWrapper = create(wrapWithTheme(<Avatar ethAddress={'0x01230123450012312'} />));
});
});

Expand Down Expand Up @@ -55,7 +55,10 @@ describe('<Avatar /> component', () => {
});
it('when not in guest mode, should load src prop', async () => {
const src = 'http://placebeard.it/640/480';
const { findByTestId } = customRender(<Avatar src={src} />, {});
const { findByTestId } = customRender(
<Avatar src={src} ethAddress={'0x01230123450012312'} />,
{},
);
const image = await waitForElement(() => findByTestId('avatar-image'));
expect(image.getAttribute('src')).toEqual(src);
});
Expand All @@ -70,7 +73,7 @@ describe('<EditableAvatar /> Component', () => {
act(() => {
componentWrapper = create(
wrapWithTheme(
<EditableAvatar guest={true} onChange={jest.fn()} src="0x1230am3421h3i14cvv21n4" />,
<EditableAvatar onChange={jest.fn()} ethAddress={'0x1230am3421h3i14cvv21n4'} />,
),
);
});
Expand All @@ -89,7 +92,7 @@ describe('<EditableAvatar /> Component', () => {
});
it('should have 1 input type file', async () => {
const { getAllByTestId } = customRender(
<EditableAvatar onChange={jest.fn()} src="0x1230am3421h3i14cvv21n4" />,
<EditableAvatar onChange={jest.fn()} ethAddress={'0x1230am3421h3i14cvv21n4'} />,
{},
);
const fileInput = await waitForElement(() => getAllByTestId('avatar-file-input'));
Expand All @@ -99,7 +102,7 @@ describe('<EditableAvatar /> Component', () => {
it('should trigger onChange event when input is changed', async () => {
const onChange = jest.fn();
const { findByTestId } = customRender(
<EditableAvatar onChange={onChange} src="0x1230am3421h3i14cvv21n4" />,
<EditableAvatar onChange={onChange} ethAddress={'0x1230am3421h3i14cvv21n4'} />,
{},
);
const fileInput = await waitForElement(() => findByTestId('avatar-file-input'));
Expand Down
39 changes: 14 additions & 25 deletions ui/design/src/components/Avatar/avatar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,13 @@ import { loadPlaceholder } from './placeholders';
import StyledAvatar, { AvatarSize } from './styled-avatar';

export interface AvatarProps extends CommonInterface<HTMLDivElement> {
src: string;
ethAddress: string;
src?: string;
onClick?: React.MouseEventHandler<any>;
alt?: string;
margin?: MarginInterface;
backgroundColor?: string;
withBorder?: boolean;
guest?: boolean;
size?: AvatarSize;
}

Expand All @@ -24,43 +24,32 @@ export const getAvatarFromSeed = (seed: string) => {
}
if (str && str.length) {
const avatarOption = Array.from(str).reduce((sum: number, letter: string) => {
if (letter.codePointAt(0)) {
return sum + letter.codePointAt(0)!;
if (parseInt(letter, 10)) {
return sum + parseInt(letter, 10);
}
return sum;
}, 0);
return (avatarOption % 7) + 1;
// if user is a visitor his address is 0x0000... so sum is 0
// so you can give him a specific placeholder (for now placeholder_7)
if (avatarOption === 0) {
return 7;
}
return (avatarOption % 6) + 1;
}
// load the first placeholder, just to not throw and error
return 1;
return 7;
};

const defaultProps: Partial<AvatarProps> = {
size: 'md' as AvatarSize,
withBorder: false,
guest: false,
src: '0x0000000000000000000000000000000',
ethAddress: '0x0000000000000000000000000000000',
};

/*
* if guest is true, render avatar in guestMode (same avatar image for all guests)
* if guest is false and src is missing or empty string, it means
* that a user (possibly registered) does not set his avatar (determine which avatar to show
* based on his eth address).
* There is one more possible case when the guest is false and src is not yet loader
* (aka. the profile data is not loaded yet), in that case, the avatar should be
* in loading state.
*/

const Avatar: React.FC<AvatarProps & typeof defaultProps> = props => {
const { onClick, guest, src, className, size, margin, withBorder } = props;
const { onClick, src, className, size, margin, withBorder, ethAddress } = props;
const isClickable = typeof onClick === 'function';
let avatarImage;
if (guest) {
avatarImage = loadPlaceholder(`placeholder_${getAvatarFromSeed(src)}`);
} else if (src) {
avatarImage = src;
}
const avatarImage = src ? src : loadPlaceholder(`placeholder_${getAvatarFromSeed(ethAddress)}`);

return (
<StyledAvatar
Expand Down
22 changes: 13 additions & 9 deletions ui/design/src/components/Avatar/placeholders/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,38 +7,42 @@ const REJECTED = 'REJECTED';
// if the tree shaking/chunking does not work!
export const loadPlaceholder = (placeholderName: string) => {
let status = PENDING;
let result: any;
let promise;
let result: string;
let promise: Promise<any>;
switch (placeholderName) {
case 'placeholder_1':
promise = import('./placeholder_1');
return;
break;
case 'placeholder_2':
promise = import('./placeholder_2');
return;
break;
case 'placeholder_3':
promise = import('./placeholder_3');
return;
break;
case 'placeholder_4':
promise = import('./placeholder_4');
break;
case 'placeholder_5':
promise = import('./placeholder_5');
break;
case 'placeholder_6':
promise = import('./placeholder_6');
break;
case 'placeholder_7':
promise = import('./placeholder_7');
break;
default:
promise = import('./placeholder_1');
}

const suspender = promise
.then(r => {
.then(res => {
status = RESOLVED;
result = r.default;
result = res.default;
})
.catch(e => {
.catch(err => {
status = REJECTED;
result = e;
result = err;
});

return {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { Box } from 'grommet';
import * as React from 'react';
import { capitalize } from '../../utils/string-utils';
import { Avatar } from '../Avatar/index';
import { AvatarSize } from '../Avatar/styled-avatar';
import { ethAddress as ethAddressType } from '../Cards/entry-box';
import StyledIconLink from './styled-icon-link';
import { capitalize } from '../../../utils/string-utils';
import { Avatar } from '../../Avatar/index';
import { AvatarSize } from '../../Avatar/styled-avatar';
import { ethAddress as ethAddressType } from '../../Cards/entry-cards/entry-box';
import StyledIconLink from '../icon-buttons/styled-icon-link';
import { ButtonInfo, ButtonTextWrapper } from './styled-profile-avatar-button';

export interface ProfileAvatarButtonProps {
Expand All @@ -15,26 +15,15 @@ export interface ProfileAvatarButtonProps {
className?: string;
onAvatarClick?: React.MouseEventHandler<ethAddressType>;
onClick?: React.MouseEventHandler<HTMLAnchorElement>;
guest?: boolean;
ethAddress: string;
}

const ProfileAvatarButton = (props: ProfileAvatarButtonProps) => {
const {
className,
size,
avatarImage,
label,
info,
onClick,
onAvatarClick,
guest,
ethAddress,
} = props;
const { className, size, avatarImage, label, info, onClick, onAvatarClick, ethAddress } = props;
return (
<Box className={className} direction="row" align="center">
<Box>
<Avatar size={size} src={avatarImage || ethAddress} onClick={onAvatarClick} guest={guest} />
<Avatar size={size} src={avatarImage} ethAddress={ethAddress} onClick={onAvatarClick} />
</Box>
<ButtonTextWrapper>
<StyledIconLink label={capitalize(label)} onClick={onClick} />
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import styled from 'styled-components';
import StyledIconLink from './styled-icon-link';
import StyledIconLink from '../icon-buttons/styled-icon-link';

const ButtonTextWrapper = styled.div`
display: flex;
Expand Down
16 changes: 16 additions & 0 deletions ui/design/src/components/Buttons/default-buttons/button.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import * as React from 'react';
import StyledButton from './styled-button';

export interface IButtonProps {
className?: string;
label: string;
onClick?: () => void;
primary?: boolean;
secondary?: boolean;
}

const Button = (props: IButtonProps) => {
return <StyledButton className={props.className} {...props} />;
};

export default Button;
20 changes: 20 additions & 0 deletions ui/design/src/components/Buttons/default-buttons/plain-button.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import * as React from 'react';
import { StyledPlainButton, StyledText } from './styled-plain-button';

export interface IPlainButtonProps {
className?: string;
onClick?: React.EventHandler<React.SyntheticEvent>;
label: string | number;
children: React.ReactNode;
}

const PlainButton = (props: IPlainButtonProps) => {
return (
<StyledPlainButton className={props.className} gap="xsmall" direction="row" align="center">
{props.children}
<StyledText onClick={props.onClick}>{props.label}</StyledText>
</StyledPlainButton>
);
};

export default PlainButton;
21 changes: 21 additions & 0 deletions ui/design/src/components/Buttons/default-buttons/styled-button.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { Button } from 'grommet';
import styled, { css } from 'styled-components';

const StyledButton = styled(Button)`
height: 2em;
border-radius: ${props => props.theme.shapes.smallBorderRadius};
color: ${props => props.theme.colors.accent};
&:hover {
box-shadow: none;
}
${props => {
if (props.primary) {
return css`
color: ${props.theme.colors.white};
`;
}
return;
}}
`;

export default StyledButton;
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { Box, Text } from 'grommet';
import styled from 'styled-components';

const StyledPlainButton = styled(Box)`
padding: 0 0.8em;
color: ${props => props.theme.colors.secondaryText};
svg {
height: 100%;
width: 1.25em;
stroke: ${props => props.theme.colors.lightGrey};
& * {
stroke: ${props => props.theme.colors.secondaryText};
}
}
`;

const StyledText = styled(Text)`
cursor: pointer;
&:hover {
color: ${props => props.theme.colors.accent};
}
`;

export { StyledPlainButton, StyledText };
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ export interface IIconButtonProps {
label: string;
onClick?: () => void;
primary?: boolean;
share?: boolean;
secondary?: boolean;
}

const IconButton = (props: IIconButtonProps) => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
import * as React from 'react';
import StyledIconLink from './styled-icon-link';

export interface LinkIconButtonProps {
export interface ILinkIconButtonProps {
className?: string;
onClick: React.EventHandler<React.SyntheticEvent>;
onClick?: React.EventHandler<React.SyntheticEvent>;
iconPosition?: 'start' | 'end';
icon: React.ReactElement;
icon?: React.ReactElement;
label: string | number;
}

const LinkIconButton = (props: LinkIconButtonProps) => {
const LinkIconButton = (props: ILinkIconButtonProps) => {
const { iconPosition = 'start', ...other } = props;
const reversed = iconPosition && iconPosition === 'end';
return <StyledIconLink className={props.className} reverse={reversed} {...other} />;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,15 @@ export interface IIconButtonProps {
icon: React.ReactNode;
onClick?: () => void;
primary?: boolean;
share?: boolean;
secondary?: boolean;
}

const StyledIconButton = styled(Button)<IIconButtonProps>`
height: 22px;
border-radius: ${props => props.theme.shapes.largeBorderRadius};
border: none;
padding: 0 0.8em;
border: 1px solid ${props => props.theme.colors.grey};
&:hover {
box-shadow: none;
border: 1px solid ${props => props.theme.colors.grey};
Expand All @@ -33,6 +34,7 @@ const StyledIconButton = styled(Button)<IIconButtonProps>`
return css`
background-color: ${props.theme.colors.accent};
color: ${props.theme.colors.white};
border: 1px solid ${props.theme.colors.accent};
svg {
stroke: ${props.theme.colors.white};
}
Expand All @@ -46,13 +48,14 @@ const StyledIconButton = styled(Button)<IIconButtonProps>`
}
`;
}
if (props.share) {
if (props.secondary) {
return css`
border-radius: ${props.theme.shapes.smallBorderRadius};
background-color: ${props.theme.colors.secondaryOpacity};
color: ${props.theme.colors.white};
border: none;
&:hover {
border: 1px solid ${props.theme.colors.secondaryOpacity};
border: none;
background-color: ${props.theme.colors.secondaryOpacity};
}
`;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,8 @@
import { Anchor } from 'grommet';
import styled from 'styled-components';
import { ILinkIconButtonProps } from './icon-link';

export interface IStyledIconLinkProps {
iconPosition?: 'start' | 'end';
size?: string;
}

const StyledIconLink = styled(Anchor)<IStyledIconLinkProps>`
const StyledIconLink = styled(Anchor)<ILinkIconButtonProps>`
border-radius: ${props => props.theme.shapes.largeBorderRadius};
border: none;
padding: 0 0.8em;
Expand Down
Loading

0 comments on commit 8dbe6a8

Please sign in to comment.