mirror of
https://github.com/LaCasemate/fab-manager.git
synced 2024-11-28 09:24:24 +01:00
(ui) avatar input
This commit is contained in:
parent
48fd47f8d9
commit
7ee4c8f4c0
@ -1,5 +1,6 @@
|
||||
import apiClient from './clients/api-client';
|
||||
import { AxiosResponse } from 'axios';
|
||||
import { serialize } from 'object-to-formdata';
|
||||
import { User, UserIndexFilter } from '../models/user';
|
||||
|
||||
export default class MemberAPI {
|
||||
@ -9,12 +10,28 @@ export default class MemberAPI {
|
||||
}
|
||||
|
||||
static async create (user: User): Promise<User> {
|
||||
const res: AxiosResponse<User> = await apiClient.post('/api/members/create', { user });
|
||||
const data = serialize({ user });
|
||||
if (user.profile_attributes.user_avatar_attributes.attachment_files[0]) {
|
||||
data.set('user[profile_attributes][user_avatar_attributes][attachment]', user.profile_attributes.user_avatar_attributes.attachment_files[0]);
|
||||
}
|
||||
const res: AxiosResponse<User> = await apiClient.post('/api/members/create', data, {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data'
|
||||
}
|
||||
});
|
||||
return res?.data;
|
||||
}
|
||||
|
||||
static async update (user: User): Promise<User> {
|
||||
const res: AxiosResponse<User> = await apiClient.patch(`/api/members/${user.id}`, { user });
|
||||
const data = serialize({ user });
|
||||
if (user.profile_attributes.user_avatar_attributes.attachment_files[0]) {
|
||||
data.set('user[profile_attributes][user_avatar_attributes][attachment]', user.profile_attributes.user_avatar_attributes.attachment_files[0]);
|
||||
}
|
||||
const res: AxiosResponse<User> = await apiClient.patch(`/api/members/${user.id}`, data, {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data'
|
||||
}
|
||||
});
|
||||
return res?.data;
|
||||
}
|
||||
}
|
||||
|
@ -58,6 +58,7 @@ export const FormInput = <TFieldValues extends FieldValues, TInputType>({ id, re
|
||||
{...register(id as FieldPath<TFieldValues>, {
|
||||
...rules,
|
||||
valueAsNumber: type === 'number',
|
||||
valueAsDate: type === 'date',
|
||||
value: defaultValue as FieldPathValue<TFieldValues, FieldPath<TFieldValues>>,
|
||||
onChange: (e) => { handleChange(e); }
|
||||
})}
|
||||
|
@ -42,7 +42,7 @@ export const RequiredTrainingModal: React.FC<RequiredTrainingModalProps> = ({ is
|
||||
const header = (): ReactNode => {
|
||||
return (
|
||||
<div className="user-info">
|
||||
<Avatar user={user} />
|
||||
<Avatar userName={user?.name} avatar={user?.profile_attributes?.user_avatar_attributes?.attachment} />
|
||||
<span className="user-name">{user?.name}</span>
|
||||
</div>
|
||||
);
|
||||
|
@ -1,29 +1,36 @@
|
||||
import React from 'react';
|
||||
import React, { useState } from 'react';
|
||||
|
||||
import noAvatar from '../../../../images/no_avatar.png';
|
||||
import { FabButton } from '../base/fab-button';
|
||||
import { Path, UseFormRegister } from 'react-hook-form';
|
||||
import { UnpackNestedValue, UseFormSetValue } from 'react-hook-form/dist/types/form';
|
||||
import { FieldPathValue } from 'react-hook-form/dist/types/path';
|
||||
import { FieldValues } from 'react-hook-form/dist/types/fields';
|
||||
import { FormInput } from '../form/form-input';
|
||||
import { Avatar } from './avatar';
|
||||
|
||||
interface AvatarInputProps<TFieldValues> {
|
||||
register: UseFormRegister<TFieldValues>,
|
||||
setValue: UseFormSetValue<TFieldValues>,
|
||||
currentAvatar: string,
|
||||
userName: string,
|
||||
size?: 'small' | 'large'
|
||||
}
|
||||
|
||||
/**
|
||||
* This component allows to set the user's avatar, in forms managed by react-hook-form.
|
||||
*/
|
||||
export const AvatarInput: React.FC = <TFieldValues extends FieldValues>({ register, setValue }: AvatarInputProps<FieldValues>) => {
|
||||
export const AvatarInput = <TFieldValues extends FieldValues>({ currentAvatar, userName, register, setValue, size }: AvatarInputProps<TFieldValues>) => {
|
||||
const [avatar, setAvatar] = useState<string|ArrayBuffer>(currentAvatar);
|
||||
/**
|
||||
* Check if the provided user has a configured avatar
|
||||
*/
|
||||
const hasAvatar = (): boolean => {
|
||||
return !!user?.profile_attributes?.user_avatar_attributes?.attachment_url;
|
||||
return !!avatar;
|
||||
};
|
||||
|
||||
/**
|
||||
* Callback triggered when the user starts to select a file.
|
||||
*/
|
||||
const onAddAvatar = (): void => {
|
||||
setValue(
|
||||
'profile_attributes.user_avatar_attributes._destroy' as Path<TFieldValues>,
|
||||
@ -31,22 +38,52 @@ export const AvatarInput: React.FC = <TFieldValues extends FieldValues>({ regist
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Callback triggered when the user clicks on the delete button.
|
||||
*/
|
||||
function onRemoveAvatar () {
|
||||
setValue(
|
||||
'profile_attributes.user_avatar_attributes._destroy' as Path<TFieldValues>,
|
||||
true as UnpackNestedValue<FieldPathValue<TFieldValues, Path<TFieldValues>>>
|
||||
);
|
||||
setAvatar(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback triggered when the user has ended its selection of a file (or when the selection has been cancelled).
|
||||
*/
|
||||
function onFileSelected (event: React.ChangeEvent<HTMLInputElement>) {
|
||||
const file = event.target?.files[0];
|
||||
if (file) {
|
||||
const reader = new FileReader();
|
||||
reader.onload = (): void => {
|
||||
setAvatar(reader.result);
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
} else {
|
||||
setAvatar(null);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`avatar ${className || ''}`}>
|
||||
{!hasAvatar() && <img src={noAvatar} alt="avatar placeholder"/>}
|
||||
{hasAvatar() && <img src={user.profile_attributes.user_avatar_attributes.attachment_url} alt="user's avatar"/>}
|
||||
{mode === 'edit' && <div className="edition">
|
||||
<FabButton onClick={onAddAvatar}>
|
||||
<div className={`avatar-input avatar-input--${size}`}>
|
||||
<Avatar avatar={avatar} userName={userName} size={size} />
|
||||
<div className="buttons">
|
||||
<FabButton onClick={onAddAvatar} className="select-button">
|
||||
{!hasAvatar() && <span>Add an avatar</span>}
|
||||
{hasAvatar() && <span>Change</span>}
|
||||
<FormInput type="file" accept="image/*" register={register} id="profile_attributes.user_avatar_attributes.attachment"/>
|
||||
<FormInput className="avatar-file-input"
|
||||
type="file"
|
||||
accept="image/*"
|
||||
register={register}
|
||||
id="profile_attributes.user_avatar_attributes.attachment_files"
|
||||
onChange={onFileSelected}/>
|
||||
</FabButton>
|
||||
{hasAvatar() && <FabButton>Remove</FabButton>}
|
||||
</div>}
|
||||
{hasAvatar() && <FabButton onClick={onRemoveAvatar} icon={<i className="fa fa-trash-o"/>} className="delete-avatar" />}
|
||||
<FormInput register={register}
|
||||
id="profile_attributes.user_avatar_attributes.id"
|
||||
type="hidden" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
Avatar.defaultProps = {
|
||||
mode: 'display'
|
||||
};
|
||||
|
@ -1,28 +1,25 @@
|
||||
import React from 'react';
|
||||
import { User } from '../../models/user';
|
||||
|
||||
import noAvatar from '../../../../images/no_avatar.png';
|
||||
|
||||
interface AvatarProps {
|
||||
user: User,
|
||||
avatar?: string | ArrayBuffer,
|
||||
userName: string,
|
||||
className?: string,
|
||||
size?: 'small' | 'large',
|
||||
}
|
||||
|
||||
/**
|
||||
* This component renders the user-profile's picture or a placeholder.
|
||||
*/
|
||||
export const Avatar: React.FC<AvatarProps> = ({ user, className }) => {
|
||||
/**
|
||||
* Check if the provided user has a configured avatar
|
||||
*/
|
||||
const hasAvatar = (): boolean => {
|
||||
return !!user?.profile_attributes?.user_avatar_attributes?.attachment_url;
|
||||
};
|
||||
|
||||
export const Avatar: React.FC<AvatarProps> = ({ avatar, className, userName, size }) => {
|
||||
return (
|
||||
<div className={`avatar ${className || ''}`}>
|
||||
{!hasAvatar() && <img src={noAvatar} alt="avatar placeholder"/>}
|
||||
{hasAvatar() && <img src={user.profile_attributes.user_avatar_attributes.attachment_url} alt="user's avatar"/>}
|
||||
<div className={`avatar avatar--${size} ${className || ''}`}>
|
||||
<img src={avatar || noAvatar} alt={userName} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
Avatar.defaultProps = {
|
||||
size: 'small'
|
||||
};
|
||||
|
@ -7,13 +7,13 @@ import { IApplication } from '../../models/application';
|
||||
import { Loader } from '../base/loader';
|
||||
import { FormInput } from '../form/form-input';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Avatar } from './avatar';
|
||||
import { GenderInput } from './gender-input';
|
||||
import { ChangePassword } from './change-password';
|
||||
import { PasswordInput } from './password-input';
|
||||
import { FormSwitch } from '../form/form-switch';
|
||||
import { FormRichText } from '../form/form-rich-text';
|
||||
import MemberAPI from '../../api/member';
|
||||
import { AvatarInput } from './avatar-input';
|
||||
|
||||
declare const Application: IApplication;
|
||||
|
||||
@ -33,7 +33,7 @@ export const UserProfileForm: React.FC<UserProfileFormProps> = ({ action, size,
|
||||
const phoneRegex = /^((00|\+)[0-9]{2,3})?[0-9]{4,14}$/;
|
||||
const urlRegex = /^(https?:\/\/)([\da-z.-]+)\.([-a-z0-9.]{2,30})([/\w .-]*)*\/?$/;
|
||||
|
||||
const { handleSubmit, register, control, formState } = useForm<User>({ defaultValues: { ...user } });
|
||||
const { handleSubmit, register, control, formState, setValue } = useForm<User>({ defaultValues: { ...user } });
|
||||
const output = useWatch<User>({ control });
|
||||
|
||||
const [isOrganization, setIsOrganization] = React.useState<boolean>(!_isNil(user.invoicing_profile_attributes.organization_attributes));
|
||||
@ -50,7 +50,11 @@ export const UserProfileForm: React.FC<UserProfileFormProps> = ({ action, size,
|
||||
return (
|
||||
<form className={`user-profile-form user-profile-form--${size} ${className}`} onSubmit={handleSubmit(onSubmit)}>
|
||||
<div className="avatar-group">
|
||||
<Avatar user={user} />
|
||||
<AvatarInput currentAvatar={output.profile_attributes.user_avatar_attributes?.attachment_url}
|
||||
userName={`${output.profile_attributes.first_name} ${output.profile_attributes.last_name}`}
|
||||
register={register}
|
||||
setValue={setValue}
|
||||
size={size} />
|
||||
</div>
|
||||
<div className="fields-group">
|
||||
<div className="personnal-data">
|
||||
|
@ -45,7 +45,10 @@ export interface User {
|
||||
flickr: string,
|
||||
user_avatar_attributes: {
|
||||
id: number,
|
||||
attachment_url: string
|
||||
attachment?: File,
|
||||
attachment_url?: string,
|
||||
attachment_files: FileList,
|
||||
_destroy?: boolean
|
||||
}
|
||||
},
|
||||
invoicing_profile_attributes: {
|
||||
|
@ -76,6 +76,7 @@
|
||||
@import "modules/subscriptions/free-extend-modal";
|
||||
@import "modules/subscriptions/renew-modal";
|
||||
@import "modules/user/avatar";
|
||||
@import "modules/user/avatar-input";
|
||||
@import "modules/user/gender-input";
|
||||
@import "modules/user/user-profile-form";
|
||||
|
||||
|
34
app/frontend/src/stylesheets/modules/user/avatar-input.scss
Normal file
34
app/frontend/src/stylesheets/modules/user/avatar-input.scss
Normal file
@ -0,0 +1,34 @@
|
||||
.avatar-input {
|
||||
margin-right: 20px;
|
||||
|
||||
.avatar {
|
||||
background-color: #fff;
|
||||
border: 1px solid var(--gray-soft);
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
.buttons {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-top: 20px;
|
||||
|
||||
.select-button {
|
||||
position: relative;
|
||||
.avatar-file-input {
|
||||
position: absolute;
|
||||
z-index: 2;
|
||||
opacity: 0;
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
}
|
||||
.delete-avatar {
|
||||
background-color: var(--error);
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
|
||||
&--large {
|
||||
margin: 80px 40px;
|
||||
}
|
||||
}
|
@ -2,5 +2,16 @@
|
||||
display: inline-block;
|
||||
img {
|
||||
border-radius: 50%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
&--small img {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
}
|
||||
|
||||
&--large img {
|
||||
width: 180px;
|
||||
height: 180px;
|
||||
}
|
||||
}
|
||||
|
@ -125,6 +125,7 @@
|
||||
"ngUpload": "0.5",
|
||||
"ngtemplate-loader": "^2.1.0",
|
||||
"nvd3": "1.8",
|
||||
"object-to-formdata": "^4.4.2",
|
||||
"phosphor-react": "^1.4.0",
|
||||
"process": "^0.11.10",
|
||||
"prop-types": "^15.7.2",
|
||||
|
@ -5297,6 +5297,11 @@ object-keys@^1.0.12, object-keys@^1.1.1:
|
||||
resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e"
|
||||
integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==
|
||||
|
||||
object-to-formdata@^4.4.2:
|
||||
version "4.4.2"
|
||||
resolved "https://registry.yarnpkg.com/object-to-formdata/-/object-to-formdata-4.4.2.tgz#f89013f90493c58cb5f6ab9f50b7aeec30745ea6"
|
||||
integrity sha512-fu6UDjsqIfFUu/B3GXJ2IFnNAL/YbsC1PPzqDIFXcfkhdYjTD3K4zqhyD/lZ6+KdP9O/64YIPckIOiS5ouXwLA==
|
||||
|
||||
object.assign@^4.1.0, object.assign@^4.1.2:
|
||||
version "4.1.2"
|
||||
resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.2.tgz#0ed54a342eceb37b38ff76eb831a0e788cb63940"
|
||||
|
Loading…
Reference in New Issue
Block a user