Skip to content

Commit

Permalink
Profile form (#289)
Browse files Browse the repository at this point in the history
* MyProfile form

* Paper, typography, inputs styled

* Flexbox in form

* Update user callback

* Submit updates user in db, updateUser changed

* Redirect

* Redirect with Link component

* Delete submission by user id

* Change password

* Responsive view

* Add photo input (#284)

* Added input button

* Files are saved in state

* Photos are convert into base64 and showed

* Rename methods

* Photos are showed in square cards

* Add styles to add button

* Added trash button

* Added delete function

* Added fromDb props and property

* Show photos from db

* Change delete size to small

* Added cb for getting photos and deleted photos

* Refactor

* Added file upload example

* Added api method for delete photo

* Rename example extension

Co-authored-by: Haivex <[email protected]>

* Composite keys, updated dummy data (#293)

* Composite keys, updated dummy data

* Get steps for user

* Update user steps

* Loading animation (#301)

* Added LoadingCircle component (center circle)

* Add loading to Gallery

* Added loading do animal info card

* Added loading to info description

* Added circle to animal info

* Added user id to visit form

* Added disabled button in login

* Delete animalId

* Added type number

* Disabled button in login

* Disable button on register

* Added bigger size circle

* Disabled button and circle in change password

* Added loading state and redirect to link page

* Added loading state to visit form

* Add loading state to survey form

* Added animalId prop

* Removed submissions

Co-authored-by: Haivex <[email protected]>

* User adoption/volunteer steps (#297)

* Redirect user to current step

* Steps automatically adds on submission

* Update adoption step on changing submission state

* Update volunteer hire steps for form submission

* Form for volunteer by step number

* Form for volunteer by step number and adoption fix

* Volunteer final step fixed

* Uncomment email code (#303)

Co-authored-by: Haivex <[email protected]>

* Adoption sign agreement (#298)

* Invitation card wwith styles

* Create page with invitation

* testing component, nth change

* Invitation card wwith styles

* Create page with invitation

* testing component, nth change

* syntax: remove unnecessary  imports

* Moved paper part to parent component

* 1st invitation page

* Routing

* Stepper borderbox

* Deleted unused import

Co-authored-by: Mateusz Król <[email protected]>
Co-authored-by: Mateusz Król <[email protected]>

* Deleted duplicated code

Co-authored-by: Haivex <[email protected]>
Co-authored-by: Haivex <[email protected]>
Co-authored-by: blackrabbit2 <[email protected]>
  • Loading branch information
4 people authored Apr 13, 2021
1 parent f4bbc45 commit bd3f725
Show file tree
Hide file tree
Showing 13 changed files with 198 additions and 52 deletions.
1 change: 1 addition & 0 deletions src/IocContainerSetup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ Container.bind(AnimalSubmissionsService).factory(
getConnection().getRepository(Animal),
getConnection().getRepository(FormAnimalAnswer),
getConnection().getRepository(OrganizationUser),
getConnection().getRepository(User),
getConnection().getRepository(AdoptionStep),
),
);
Expand Down
15 changes: 15 additions & 0 deletions src/application/AnimalSubmissionsService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import FormAnimalAnswer from '@infrastructure/postgres/FormAnimalAnswer';
import FormAnimalSubmission, { AnimalFormStatus } from '@infrastructure/postgres/FormAnimalSubmission';
import { AnswerForm } from '@infrastructure/postgres/FormQuestion';
import OrganizationUser, { UserType } from '@infrastructure/postgres/OrganizationUser';
import User from '@infrastructure/postgres/User';
import { Repository } from 'typeorm';
import OptionalWhereSelectQueryBuilder from 'utils/OptionalWhereSelectQueryBuilder';
import { Inject } from 'typescript-ioc';
Expand Down Expand Up @@ -67,6 +68,7 @@ export class AnimalSubmissionsService {
private animalRepository: Repository<Animal>,
private animalAnswerRepository: Repository<FormAnimalAnswer>,
private organizationUserRepository: Repository<OrganizationUser>,
private userRepository: Repository<User>,
private adoptionStepRepository: Repository<AdoptionStep>,
) {}
@Inject
Expand Down Expand Up @@ -280,4 +282,17 @@ export class AnimalSubmissionsService {
if (nextSubmission) await this.usersService.updateFormSteps(user.id, { adoptionStep: stepNumber + 1 }, user);
await this.animalSubmissionRepository.save(submission);
}

public async deleteAnimalSubmission(userId: number, request: IAuthUserInfoRequest): Promise<void> {
const currentUser = request.user as IUserInfo;
const user = await this.userRepository.findOne(userId);
if (!user) {
throw new ApiError('Not found', 404, 'User does not exist');
}

if (currentUser.role === UserType.ADMIN || currentUser.id === userId) {
await this.animalSubmissionRepository.delete({ applicant: { id: userId } });
return;
}
}
}
2 changes: 1 addition & 1 deletion src/application/UsersService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ export type UserResetPasswordParams = {
repPassword: Password;
};

export type UserUpdateParams = Pick<User, 'name' | 'phone' | 'surname'>;
export type UserUpdateParams = Pick<User, 'name' | 'phone' | 'surname' | 'birthDate' | 'mail'>;

export type UserGetFormSteps = Pick<User, 'adoptionStep' | 'volunteerStep'>;

Expand Down
2 changes: 1 addition & 1 deletion src/infrastructure/postgres/FormAnimalSubmission.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ export default class FormAnimalSubmission {
@ManyToOne(() => OrganizationUser, (user) => user.animalReviews, { nullable: true })
reviewer?: OrganizationUser;

@OneToMany(() => FormAnimalAnswer, (answers) => answers.submission, { cascade: true })
@OneToMany(() => FormAnimalAnswer, (answers) => answers.submission)
answers!: FormAnimalAnswer[];

@IsDate()
Expand Down
7 changes: 1 addition & 6 deletions src/infrastructure/postgres/User.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,20 +101,15 @@ export default class User {

@OneToMany(() => FormAnimalSubmission, (submission) => submission.applicant, {
nullable: true,
onDelete: 'CASCADE',
})
animalSubmissions!: FormAnimalSubmission[];
animalSubmissions?: FormAnimalSubmission[];

@OneToMany(() => OrganizationUser, (organizationUser) => organizationUser.user, {
nullable: true,
cascade: true,
onDelete: 'CASCADE',
})
organizationUsers?: OrganizationUser[];

@OneToMany(() => FormVolunteerSubmission, (submission) => submission.user, {
cascade: true,
onDelete: 'CASCADE',
nullable: true,
})
volunteerSubmission?: FormVolunteerSubmission[];
Expand Down
13 changes: 13 additions & 0 deletions src/presentation/rest/AnimalSubmissionsController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
Security,
Post,
Request,
Delete,
} from 'tsoa';
import { Inject } from 'typescript-ioc';
import { IAuthUserInfoRequest, IUserInfo } from '@infrastructure/Auth';
Expand Down Expand Up @@ -130,4 +131,16 @@ export class AnimalSubmissionsController extends Controller {
await this.submissionService.createAnimalSubmission(requestBody, request);
this.setStatus(204);
}

@Security('jwt', ['admin', 'normal', 'volunteer', 'employee'])
@Response<ApiError>(404, 'Not Found')
@SuccessResponse(204, 'Deleted')
@Delete('{userId}')
public async deleteAnimalSubmission(
@Path() userId: number,
@Request() request: IAuthUserInfoRequest,
): Promise<void> {
await this.deleteAnimalSubmission(userId, request);
this.setStatus(204);
}
}
2 changes: 1 addition & 1 deletion src/presentation/rest/UsersController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -232,8 +232,8 @@ export class UsersController extends Controller {
@SuccessResponse(200, ' User deleted')
@Delete('{userId}')
public async deleteUser(@Path() userId: number, @Request() request: IAuthUserInfoRequest): Promise<void> {
this.setStatus(200);
await this.usersService.delete(userId, request);
this.setStatus(200);
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React from 'react';
import React, { useContext } from 'react';
import { useForm } from 'react-hook-form';
import { Theme, makeStyles } from '@material-ui/core';
import Button from '@material-ui/core/Button';
Expand All @@ -8,6 +8,8 @@ import TextField from '@material-ui/core/TextField';
import { Link as RouterLink } from 'react-router-dom';
import LoadingCircleSmall from '../../loadingCircleSmall/LoadingCircleSmall';
import AuthPaper from '../authPaper/AuthPaper';
import { useUpdateUserPassword } from '../../../client';
import { AppCtx } from '../../../App';


interface Inputs {
Expand Down Expand Up @@ -41,6 +43,8 @@ const useStyle = makeStyles<Theme>((theme) => ({

const ChangePassword: React.FC = () => {
const classes = useStyle();
const { appState } = useContext(AppCtx);
const { mutate: changePassword } = useUpdateUserPassword({ userId: appState.userId!, requestOptions: { headers: { access_token: localStorage.getItem('apiKey') ?? '' } } })

const passwordPattern = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,}$/;
const { register, errors, trigger, formState, getValues, handleSubmit } = useForm<Inputs>();
Expand All @@ -50,9 +54,9 @@ const ChangePassword: React.FC = () => {
}
const repeatPassword = (value: string) => value === getValues().password || 'Hasła muszą być takie same!'

const onSubmit = async ({ password, repPassword }: Inputs) => {
const onSubmit = async (data: Inputs) => {
try {
console.log(password, repPassword);
await changePassword(data);
} catch (error) {
console.error(error);
}
Expand Down
104 changes: 104 additions & 0 deletions src/presentation/web/src/components/auth/profilePage/ProfilePage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import React, { useContext } from 'react';
import RegisterForm from '../../forms/registerForm/RegisterForm';
import { AppCtx } from '../../../App';
import { useDeleteUser, useGetUser, useUpdateUser } from '../../../client';
import Button from '@material-ui/core/Button';
import Paper from '@material-ui/core/Paper';
import Typography from '@material-ui/core/Typography';
import { Theme, makeStyles } from '@material-ui/core';
import SvgIcon from '@material-ui/core/SvgIcon'
import PersonOutlineRoundedIcon from '@material-ui/icons/PersonOutlineRounded';
import { Link } from 'react-router-dom';

const useStyles = makeStyles<Theme>((theme: Theme) => ({
paper: {
backgroundColor: theme.palette.background.paper,
padding: '2rem 4rem',
display: 'flex',
flexDirection: 'column',
justifyContent: 'space-between',
alignItems: 'center',
[theme.breakpoints.down('sm')]: {
padding: '1rem 2rem'
}
},
text: {
color: theme.palette.text.primary,
marginBottom: '2rem'
},
lockBackground: {
backgroundColor: theme.palette.secondary.dark,
borderRadius: 90,
padding: 8,
marginBottom: 10
},
lockIcon: {
color: '#FFF',
opacity: .87,
},
button: {
marginBottom: '2rem',
minWidth: 300,
[theme.breakpoints.down('sm')]: {
width: '70%',
minWidth: 170
}
}
}))

const ProfilePage = () => {
const apiKey = { requestOptions: { headers: { access_token: localStorage.getItem('apiKey') ?? '' } } };
const classes = useStyles();
const { appState, setAppState } = useContext(AppCtx);
const { data: userData, loading } = useGetUser({ userId: appState.userId!, ...apiKey});
const { mutate: updateUser } = useUpdateUser({ userId: appState.userId!, ...apiKey});
const { mutate: deleteUser } = useDeleteUser({ ...apiKey })

const handleSubmit = async (data: any) => {
try {
await updateUser(data);
} catch (error) {
console.error(error);
}
}
const handleDeleteAccountButton = async () => {
try {
console.log('Nie działa')
// await deleteUser(appState.userId!);
} catch (error) {
console.error(error);
}
}

if (!loading && userData && userData.birthDate) {
const [year, month, date] = userData.birthDate.split('-').map((value) => parseInt(value))
return (
<Paper className={classes.paper}>
<SvgIcon className={classes.lockBackground}>
<PersonOutlineRoundedIcon className={classes.lockIcon} />
</SvgIcon>
<Typography className={classes.text} variant="h4">Mój profil</Typography>
<RegisterForm handleSubmit={handleSubmit} defaultValues={{
name: userData.name,
surname: userData.surname,
mail: userData.mail,
phone: userData.phone,
birthDate: new Date(year, month, date)
}} hiddenPassword={true}>
<Button component={Link} to="/auth/change" className={classes.button} size="large" variant="outlined" color="primary">
Zmień hasło
</Button>
<Button className={classes.button} size="large" variant="outlined" color="primary" onClick={handleDeleteAccountButton}>
Usuń konto
</Button>
<Button className={classes.button} size="large" variant="contained" color="primary" type="submit">
Zapisz zmiany
</Button>
</RegisterForm>
</Paper>
)
}
return <div>loading</div>
}

export default ProfilePage
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export interface Inputs {
interface Props {
handleSubmit: (data: any) => Promise<any>;
defaultValues?: Partial<Inputs>;
hiddenPassword?: boolean;
}

const useStyle = makeStyles({
Expand All @@ -38,10 +39,17 @@ const useStyle = makeStyles({
},
paddingBottom: 50,
position: 'relative'
},
form: {
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
alignItems: 'center',
width: '100%'
}
})

const RegisterForm: React.FC<Props> = ({ handleSubmit: submitCb, defaultValues }) => {
const RegisterForm: React.FC<Props> = ({ handleSubmit: submitCb, defaultValues, hiddenPassword = false, children}) => {
const classes = useStyle();

const emailPattern = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
Expand Down Expand Up @@ -74,7 +82,7 @@ const RegisterForm: React.FC<Props> = ({ handleSubmit: submitCb, defaultValues }

return (
<MuiPickersUtilsProvider utils={DateFnsUtils} locale={plLocale}>
<form onSubmit={handleSubmit(submitCb)} noValidate>
<form className={classes.form} onSubmit={handleSubmit(submitCb)} noValidate>
<TextField
className={classes.textField}
id="name"
Expand Down Expand Up @@ -109,31 +117,35 @@ const RegisterForm: React.FC<Props> = ({ handleSubmit: submitCb, defaultValues }
helperText={errors.mail && errors.mail.message}
data-testid="mailInput"
/>
<TextField
className={classes.textField}
id="password"
name="password"
label="Hasło"
type="password"
required
onChange={validateRepeatPassword}
inputRef={register({ required: 'Hasło jest wymagane!', pattern: { value: passwordPattern, message: 'Hasło musi zawierać co najmniej 8 znaków, jedną małą literę, jedną wielką literę, jedną liczbę oraz jeden znak specjalny (@$!%*?&)!' } })}
error={errors.hasOwnProperty('password')}
helperText={errors.password && errors.password.message}
data-testid="passwordInput"
/>
<TextField
className={classes.textField}
id="repPassword"
name="repPassword"
label="Powtórz hasło"
type="password"
required
inputRef={register({ required: 'Powtórzone hasło jest wymagane!', validate: { repeatPassword } })}
error={errors.hasOwnProperty('repPassword')}
helperText={errors.repPassword && errors.repPassword.message}
data-testid="repPasswordInput"
/>
{!hiddenPassword &&
<>
<TextField
className={classes.textField}
id="password"
name="password"
label="Hasło"
type="password"
required
onChange={validateRepeatPassword}
inputRef={register({ required: 'Hasło jest wymagane!', pattern: { value: passwordPattern, message: 'Hasło musi zawierać co najmniej 8 znaków, jedną małą literę, jedną wielką literę, jedną liczbę oraz jeden znak specjalny (@$!%*?&)!' } })}
error={errors.hasOwnProperty('password')}
helperText={errors.password && errors.password.message}
data-testid="passwordInput"
/>
<TextField
className={classes.textField}
id="repPassword"
name="repPassword"
label="Powtórz hasło"
type="password"
required
inputRef={register({ required: 'Powtórzone hasło jest wymagane!', validate: { repeatPassword } })}
error={errors.hasOwnProperty('repPassword')}
helperText={errors.repPassword && errors.repPassword.message}
data-testid="repPasswordInput"
/>
</>
}
<KeyboardDatePicker
disableFuture
required
Expand Down Expand Up @@ -166,17 +178,18 @@ const RegisterForm: React.FC<Props> = ({ handleSubmit: submitCb, defaultValues }
helperText={errors.phone && errors.phone.message}
data-testid="phoneInput"
/>
<Button
disabled={formState.isSubmitting}
className={classes.submit}
fullWidth
size="large"
variant="contained"
color="primary"
type="submit"
data-testid="formSubmit">
{children ??
<Button
disabled={formState.isSubmitting}
className={classes.submit}
fullWidth
size="large"
variant="contained"
color="primary"
type="submit"
data-testid="formSubmit">
Zarejestruj się {formState.isSubmitting && <LoadingCircleSmall size={20} />}
</Button>
</Button>}
</form>
</MuiPickersUtilsProvider>
)
Expand Down
2 changes: 1 addition & 1 deletion src/presentation/web/src/pages/Auth.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ const Auth: React.FC = () => {
const { path } = useRouteMatch();

return (
<GridContainer spacing={2} align="center" justify="center">
<GridContainer spacing={2} align="center" justify="center">``
<Switch>
<Route exact path={`${path}/link`} component={ActivationSent} />
<Route exact path={`${path}/register`} component={RegisterPage} />
Expand Down
Loading

0 comments on commit bd3f725

Please sign in to comment.