diff options
author | Chocobozzz <me@florianbigard.com> | 2019-05-29 11:03:01 +0200 |
---|---|---|
committer | Chocobozzz <me@florianbigard.com> | 2019-05-29 11:19:54 +0200 |
commit | 1d5342abc43df02cf0bd69b1e865c0f179182eef (patch) | |
tree | f008b43ca8189ee5856e39e5b9d4800bf37f9575 | |
parent | e590b4a512617bbf63595b684386f68abea7d8b8 (diff) | |
download | PeerTube-1d5342abc43df02cf0bd69b1e865c0f179182eef.tar.gz PeerTube-1d5342abc43df02cf0bd69b1e865c0f179182eef.tar.zst PeerTube-1d5342abc43df02cf0bd69b1e865c0f179182eef.zip |
Multi step registration
19 files changed, 502 insertions, 93 deletions
diff --git a/client/src/app/shared/forms/peertube-checkbox.component.scss b/client/src/app/shared/forms/peertube-checkbox.component.scss index ea321ee65..84ea788af 100644 --- a/client/src/app/shared/forms/peertube-checkbox.component.scss +++ b/client/src/app/shared/forms/peertube-checkbox.component.scss | |||
@@ -14,9 +14,6 @@ | |||
14 | 14 | ||
15 | input { | 15 | input { |
16 | @include peertube-checkbox(1px); | 16 | @include peertube-checkbox(1px); |
17 | |||
18 | width: 10px; | ||
19 | margin-right: 10px; | ||
20 | } | 17 | } |
21 | } | 18 | } |
22 | 19 | ||
diff --git a/client/src/app/shared/users/user.service.ts b/client/src/app/shared/users/user.service.ts index cc5c051f1..20883456f 100644 --- a/client/src/app/shared/users/user.service.ts +++ b/client/src/app/shared/users/user.service.ts | |||
@@ -9,6 +9,7 @@ import { Avatar } from '../../../../../shared/models/avatars/avatar.model' | |||
9 | import { SortMeta } from 'primeng/api' | 9 | import { SortMeta } from 'primeng/api' |
10 | import { BytesPipe } from 'ngx-pipes' | 10 | import { BytesPipe } from 'ngx-pipes' |
11 | import { I18n } from '@ngx-translate/i18n-polyfill' | 11 | import { I18n } from '@ngx-translate/i18n-polyfill' |
12 | import { UserRegister } from '@shared/models/users/user-register.model' | ||
12 | 13 | ||
13 | @Injectable() | 14 | @Injectable() |
14 | export class UserService { | 15 | export class UserService { |
@@ -64,7 +65,7 @@ export class UserService { | |||
64 | .pipe(catchError(err => this.restExtractor.handleError(err))) | 65 | .pipe(catchError(err => this.restExtractor.handleError(err))) |
65 | } | 66 | } |
66 | 67 | ||
67 | signup (userCreate: UserCreate) { | 68 | signup (userCreate: UserRegister) { |
68 | return this.authHttp.post(UserService.BASE_USERS_URL + 'register', userCreate) | 69 | return this.authHttp.post(UserService.BASE_USERS_URL + 'register', userCreate) |
69 | .pipe( | 70 | .pipe( |
70 | map(this.restExtractor.extractDataBool), | 71 | map(this.restExtractor.extractDataBool), |
diff --git a/client/src/app/signup/custom-stepper.component.html b/client/src/app/signup/custom-stepper.component.html new file mode 100644 index 000000000..bf507fc4f --- /dev/null +++ b/client/src/app/signup/custom-stepper.component.html | |||
@@ -0,0 +1,25 @@ | |||
1 | <section class="container"> | ||
2 | <header> | ||
3 | <ng-container *ngFor="let step of steps; let i = index; let isLast = last;"> | ||
4 | <div | ||
5 | class="step-info" [ngClass]="{ active: selectedIndex === i, completed: isCompleted(step) }" | ||
6 | (click)="onClick(i)" | ||
7 | > | ||
8 | <div class="step-index"> | ||
9 | <ng-container *ngIf="!isCompleted(step)">{{ i + 1 }}</ng-container> | ||
10 | <my-global-icon *ngIf="isCompleted(step)" iconName="tick"></my-global-icon> | ||
11 | </div> | ||
12 | |||
13 | <div class="step-label">{{ step.label }}</div> | ||
14 | </div> | ||
15 | |||
16 | <!-- Do no display if this is the last child --> | ||
17 | <div *ngIf="!isLast" class="connector"></div> | ||
18 | </ng-container> | ||
19 | </header> | ||
20 | |||
21 | <div [style.display]="selected ? 'block' : 'none'"> | ||
22 | <ng-container [ngTemplateOutlet]="selected.content"></ng-container> | ||
23 | </div> | ||
24 | |||
25 | </section> | ||
diff --git a/client/src/app/signup/custom-stepper.component.scss b/client/src/app/signup/custom-stepper.component.scss new file mode 100644 index 000000000..2371c8ae5 --- /dev/null +++ b/client/src/app/signup/custom-stepper.component.scss | |||
@@ -0,0 +1,66 @@ | |||
1 | @import '_variables'; | ||
2 | @import '_mixins'; | ||
3 | |||
4 | $grey-color: #9CA3AB; | ||
5 | $index-block-height: 32px; | ||
6 | |||
7 | header { | ||
8 | display: flex; | ||
9 | justify-content: space-between; | ||
10 | font-size: 15px; | ||
11 | margin-bottom: 30px; | ||
12 | |||
13 | .step-info { | ||
14 | color: $grey-color; | ||
15 | display: flex; | ||
16 | flex-direction: column; | ||
17 | align-items: center; | ||
18 | width: $index-block-height; | ||
19 | |||
20 | .step-index { | ||
21 | display: flex; | ||
22 | justify-content: center; | ||
23 | align-items: center; | ||
24 | width: $index-block-height; | ||
25 | height: $index-block-height; | ||
26 | border-radius: 100px; | ||
27 | border: 2px solid $grey-color; | ||
28 | margin-bottom: 10px; | ||
29 | |||
30 | my-global-icon { | ||
31 | @include apply-svg-color(var(--mainBackgroundColor)); | ||
32 | |||
33 | width: 22px; | ||
34 | height: 22px; | ||
35 | } | ||
36 | } | ||
37 | |||
38 | .step-label { | ||
39 | width: max-content; | ||
40 | } | ||
41 | |||
42 | &.active, | ||
43 | &.completed { | ||
44 | .step-index { | ||
45 | border-color: var(--mainColor); | ||
46 | background-color: var(--mainColor); | ||
47 | color: var(--mainBackgroundColor); | ||
48 | } | ||
49 | |||
50 | .step-label { | ||
51 | color: var(--mainColor); | ||
52 | } | ||
53 | } | ||
54 | |||
55 | &.completed { | ||
56 | cursor: pointer; | ||
57 | } | ||
58 | } | ||
59 | |||
60 | .connector { | ||
61 | flex: auto; | ||
62 | margin: $index-block-height/2 10px 0 10px; | ||
63 | height: 2px; | ||
64 | background-color: $grey-color; | ||
65 | } | ||
66 | } | ||
diff --git a/client/src/app/signup/custom-stepper.component.ts b/client/src/app/signup/custom-stepper.component.ts new file mode 100644 index 000000000..2ae40f3a9 --- /dev/null +++ b/client/src/app/signup/custom-stepper.component.ts | |||
@@ -0,0 +1,19 @@ | |||
1 | import { Component } from '@angular/core' | ||
2 | import { CdkStep, CdkStepper } from '@angular/cdk/stepper' | ||
3 | |||
4 | @Component({ | ||
5 | selector: 'my-custom-stepper', | ||
6 | templateUrl: './custom-stepper.component.html', | ||
7 | styleUrls: [ './custom-stepper.component.scss' ], | ||
8 | providers: [ { provide: CdkStepper, useExisting: CustomStepperComponent } ] | ||
9 | }) | ||
10 | export class CustomStepperComponent extends CdkStepper { | ||
11 | |||
12 | onClick (index: number): void { | ||
13 | this.selectedIndex = index | ||
14 | } | ||
15 | |||
16 | isCompleted (step: CdkStep) { | ||
17 | return step.stepControl && step.stepControl.dirty && step.stepControl.valid | ||
18 | } | ||
19 | } | ||
diff --git a/client/src/app/signup/signup-step-channel.component.html b/client/src/app/signup/signup-step-channel.component.html new file mode 100644 index 000000000..68ea4473a --- /dev/null +++ b/client/src/app/signup/signup-step-channel.component.html | |||
@@ -0,0 +1,50 @@ | |||
1 | <form role="form" [formGroup]="form"> | ||
2 | |||
3 | <div class="channel-explanations"> | ||
4 | <p i18n> | ||
5 | A channel is an entity in which you upload your videos. Creating several of them helps you to organize and separate your content.<br /> | ||
6 | For example, you could decide to have a channel to publish your piano concerts, and another channel in which you publish your videos talking about ecology. | ||
7 | </p> | ||
8 | |||
9 | <p> | ||
10 | Other users can decide to subscribe any channel they want, to be notified when you publish a new video. | ||
11 | </p> | ||
12 | </div> | ||
13 | |||
14 | <div class="form-group"> | ||
15 | <label for="name" i18n>Channel name</label> | ||
16 | |||
17 | <div class="input-group"> | ||
18 | <input | ||
19 | type="text" id="name" i18n-placeholder placeholder="Example: my_super_channel" | ||
20 | formControlName="name" [ngClass]="{ 'input-error': formErrors['name'] }" | ||
21 | > | ||
22 | <div class="input-group-append"> | ||
23 | <span class="input-group-text">@{{ instanceHost }}</span> | ||
24 | </div> | ||
25 | </div> | ||
26 | |||
27 | <div *ngIf="formErrors.name" class="form-error"> | ||
28 | {{ formErrors.name }} | ||
29 | </div> | ||
30 | |||
31 | <div *ngIf="isSameThanUsername()" class="form-error" i18n> | ||
32 | Channel name cannot be the same than your account name. You can click on the first step to update your account name. | ||
33 | </div> | ||
34 | </div> | ||
35 | |||
36 | <div class="form-group"> | ||
37 | <label for="displayName" i18n>Channel display name</label> | ||
38 | |||
39 | <div class="input-group"> | ||
40 | <input | ||
41 | type="text" id="displayName" | ||
42 | formControlName="displayName" [ngClass]="{ 'input-error': formErrors['displayName'] }" | ||
43 | > | ||
44 | </div> | ||
45 | |||
46 | <div *ngIf="formErrors.displayName" class="form-error"> | ||
47 | {{ formErrors.displayName }} | ||
48 | </div> | ||
49 | </div> | ||
50 | </form> | ||
diff --git a/client/src/app/signup/signup-step-channel.component.ts b/client/src/app/signup/signup-step-channel.component.ts new file mode 100644 index 000000000..a49b7f36f --- /dev/null +++ b/client/src/app/signup/signup-step-channel.component.ts | |||
@@ -0,0 +1,40 @@ | |||
1 | import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core' | ||
2 | import { AuthService } from '@app/core' | ||
3 | import { FormReactive, VideoChannelValidatorsService } from '../shared' | ||
4 | import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service' | ||
5 | import { FormGroup } from '@angular/forms' | ||
6 | |||
7 | @Component({ | ||
8 | selector: 'my-signup-step-channel', | ||
9 | templateUrl: './signup-step-channel.component.html', | ||
10 | styleUrls: [ './signup.component.scss' ] | ||
11 | }) | ||
12 | export class SignupStepChannelComponent extends FormReactive implements OnInit { | ||
13 | @Input() username: string | ||
14 | @Output() formBuilt = new EventEmitter<FormGroup>() | ||
15 | |||
16 | constructor ( | ||
17 | protected formValidatorService: FormValidatorService, | ||
18 | private authService: AuthService, | ||
19 | private videoChannelValidatorsService: VideoChannelValidatorsService | ||
20 | ) { | ||
21 | super() | ||
22 | } | ||
23 | |||
24 | get instanceHost () { | ||
25 | return window.location.host | ||
26 | } | ||
27 | |||
28 | isSameThanUsername () { | ||
29 | return this.username && this.username === this.form.value['name'] | ||
30 | } | ||
31 | |||
32 | ngOnInit () { | ||
33 | this.buildForm({ | ||
34 | name: this.videoChannelValidatorsService.VIDEO_CHANNEL_NAME, | ||
35 | displayName: this.videoChannelValidatorsService.VIDEO_CHANNEL_DISPLAY_NAME | ||
36 | }) | ||
37 | |||
38 | setTimeout(() => this.formBuilt.emit(this.form)) | ||
39 | } | ||
40 | } | ||
diff --git a/client/src/app/signup/signup-step-user.component.html b/client/src/app/signup/signup-step-user.component.html new file mode 100644 index 000000000..cd0c78bfa --- /dev/null +++ b/client/src/app/signup/signup-step-user.component.html | |||
@@ -0,0 +1,54 @@ | |||
1 | <form role="form" [formGroup]="form"> | ||
2 | |||
3 | <div class="form-group"> | ||
4 | <label for="username" i18n>Username</label> | ||
5 | |||
6 | <div class="input-group"> | ||
7 | <input | ||
8 | type="text" id="username" i18n-placeholder placeholder="Example: jane_doe" | ||
9 | formControlName="username" [ngClass]="{ 'input-error': formErrors['username'] }" | ||
10 | > | ||
11 | <div class="input-group-append"> | ||
12 | <span class="input-group-text">@{{ instanceHost }}</span> | ||
13 | </div> | ||
14 | </div> | ||
15 | |||
16 | <div *ngIf="formErrors.username" class="form-error"> | ||
17 | {{ formErrors.username }} | ||
18 | </div> | ||
19 | </div> | ||
20 | |||
21 | <div class="form-group"> | ||
22 | <label for="email" i18n>Email</label> | ||
23 | <input | ||
24 | type="text" id="email" i18n-placeholder placeholder="Email" | ||
25 | formControlName="email" [ngClass]="{ 'input-error': formErrors['email'] }" | ||
26 | > | ||
27 | <div *ngIf="formErrors.email" class="form-error"> | ||
28 | {{ formErrors.email }} | ||
29 | </div> | ||
30 | </div> | ||
31 | |||
32 | <div class="form-group"> | ||
33 | <label for="password" i18n>Password</label> | ||
34 | <input | ||
35 | type="password" id="password" i18n-placeholder placeholder="Password" | ||
36 | formControlName="password" [ngClass]="{ 'input-error': formErrors['password'] }" | ||
37 | > | ||
38 | <div *ngIf="formErrors.password" class="form-error"> | ||
39 | {{ formErrors.password }} | ||
40 | </div> | ||
41 | </div> | ||
42 | |||
43 | <div class="form-group form-group-terms"> | ||
44 | <my-peertube-checkbox | ||
45 | inputName="terms" formControlName="terms" | ||
46 | i18n-labelHtml | ||
47 | labelHtml="I am at least 16 years old and agree to the <a href='/about/instance#terms-section' target='_blank'rel='noopener noreferrer'>Terms</a> of this instance" | ||
48 | ></my-peertube-checkbox> | ||
49 | |||
50 | <div *ngIf="formErrors.terms" class="form-error"> | ||
51 | {{ formErrors.terms }} | ||
52 | </div> | ||
53 | </div> | ||
54 | </form> | ||
diff --git a/client/src/app/signup/signup-step-user.component.ts b/client/src/app/signup/signup-step-user.component.ts new file mode 100644 index 000000000..54855d8a7 --- /dev/null +++ b/client/src/app/signup/signup-step-user.component.ts | |||
@@ -0,0 +1,37 @@ | |||
1 | import { Component, EventEmitter, OnInit, Output } from '@angular/core' | ||
2 | import { AuthService } from '@app/core' | ||
3 | import { FormReactive, UserValidatorsService } from '../shared' | ||
4 | import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service' | ||
5 | import { FormGroup } from '@angular/forms' | ||
6 | |||
7 | @Component({ | ||
8 | selector: 'my-signup-step-user', | ||
9 | templateUrl: './signup-step-user.component.html', | ||
10 | styleUrls: [ './signup.component.scss' ] | ||
11 | }) | ||
12 | export class SignupStepUserComponent extends FormReactive implements OnInit { | ||
13 | @Output() formBuilt = new EventEmitter<FormGroup>() | ||
14 | |||
15 | constructor ( | ||
16 | protected formValidatorService: FormValidatorService, | ||
17 | private authService: AuthService, | ||
18 | private userValidatorsService: UserValidatorsService | ||
19 | ) { | ||
20 | super() | ||
21 | } | ||
22 | |||
23 | get instanceHost () { | ||
24 | return window.location.host | ||
25 | } | ||
26 | |||
27 | ngOnInit () { | ||
28 | this.buildForm({ | ||
29 | username: this.userValidatorsService.USER_USERNAME, | ||
30 | password: this.userValidatorsService.USER_PASSWORD, | ||
31 | email: this.userValidatorsService.USER_EMAIL, | ||
32 | terms: this.userValidatorsService.USER_TERMS | ||
33 | }) | ||
34 | |||
35 | setTimeout(() => this.formBuilt.emit(this.form)) | ||
36 | } | ||
37 | } | ||
diff --git a/client/src/app/signup/signup.component.html b/client/src/app/signup/signup.component.html index 07d24b381..ae3a595e9 100644 --- a/client/src/app/signup/signup.component.html +++ b/client/src/app/signup/signup.component.html | |||
@@ -4,64 +4,35 @@ | |||
4 | Create an account | 4 | Create an account |
5 | </div> | 5 | </div> |
6 | 6 | ||
7 | <div *ngIf="info" class="alert alert-info">{{ info }}</div> | ||
8 | <div *ngIf="error" class="alert alert-danger">{{ error }}</div> | ||
9 | |||
10 | <div class="d-flex justify-content-left flex-wrap"> | ||
11 | <form role="form" (ngSubmit)="signup()" [formGroup]="form"> | ||
12 | <div class="form-group"> | ||
13 | <label for="username" i18n>Username</label> | ||
14 | |||
15 | <div class="input-group"> | ||
16 | <input | ||
17 | type="text" id="username" i18n-placeholder placeholder="Example: jane_doe" | ||
18 | formControlName="username" [ngClass]="{ 'input-error': formErrors['username'] }" | ||
19 | > | ||
20 | <div class="input-group-append"> | ||
21 | <span class="input-group-text">@{{ instanceHost }}</span> | ||
22 | </div> | ||
23 | </div> | ||
24 | |||
25 | <div *ngIf="formErrors.username" class="form-error"> | ||
26 | {{ formErrors.username }} | ||
27 | </div> | ||
28 | </div> | ||
29 | 7 | ||
30 | <div class="form-group"> | 8 | <my-success *ngIf="signupDone"></my-success> |
31 | <label for="email" i18n>Email</label> | 9 | <div *ngIf="info" class="alert alert-info">{{ info }}</div> |
32 | <input | 10 | <div *ngIf="success" class="alert alert-success">{{ success }}</div> |
33 | type="text" id="email" i18n-placeholder placeholder="Email" | ||
34 | formControlName="email" [ngClass]="{ 'input-error': formErrors['email'] }" | ||
35 | > | ||
36 | <div *ngIf="formErrors.email" class="form-error"> | ||
37 | {{ formErrors.email }} | ||
38 | </div> | ||
39 | </div> | ||
40 | 11 | ||
41 | <div class="form-group"> | 12 | <div class="wrapper" *ngIf="!signupDone"> |
42 | <label for="password" i18n>Password</label> | 13 | <div> |
43 | <input | 14 | <my-custom-stepper linear *ngIf="!signupDone"> |
44 | type="password" id="password" i18n-placeholder placeholder="Password" | 15 | <cdk-step [stepControl]="formStepUser" i18n-label label="User information"> |
45 | formControlName="password" [ngClass]="{ 'input-error': formErrors['password'] }" | 16 | <my-signup-step-user (formBuilt)="onUserFormBuilt($event)"></my-signup-step-user> |
46 | > | ||
47 | <div *ngIf="formErrors.password" class="form-error"> | ||
48 | {{ formErrors.password }} | ||
49 | </div> | ||
50 | </div> | ||
51 | 17 | ||
52 | <div class="form-group form-group-terms"> | 18 | <button i18n cdkStepperNext [disabled]="!formStepUser || !formStepUser.valid">Next</button> |
53 | <my-peertube-checkbox | 19 | </cdk-step> |
54 | inputName="terms" formControlName="terms" | ||
55 | i18n-labelHtml labelHtml="I am at least 16 years old and agree to the <a href='/about/instance#terms-section' target='_blank'rel='noopener noreferrer'>Terms</a> of this instance" | ||
56 | ></my-peertube-checkbox> | ||
57 | 20 | ||
58 | <div *ngIf="formErrors.terms" class="form-error"> | 21 | <cdk-step [stepControl]="formStepChannel" i18n-label label="Channel information"> |
59 | {{ formErrors.terms }} | 22 | <my-signup-step-channel (formBuilt)="onChannelFormBuilt($event)" [username]="getUsername()"></my-signup-step-channel> |
60 | </div> | ||
61 | </div> | ||
62 | 23 | ||
63 | <input type="submit" i18n-value value="Signup" [disabled]="!form.valid || signupDone"> | 24 | <button i18n cdkStepperNext (click)="signup()" |
64 | </form> | 25 | [disabled]="!formStepChannel || !formStepChannel.valid || hasSameChannelAndAccountNames()" |
26 | > | ||
27 | Create my account | ||
28 | </button> | ||
29 | </cdk-step> | ||
30 | |||
31 | <cdk-step i18n-label label="Done" editable="false"> | ||
32 | <div *ngIf="error" class="alert alert-danger">{{ error }}</div> | ||
33 | </cdk-step> | ||
34 | </my-custom-stepper> | ||
35 | </div> | ||
65 | 36 | ||
66 | <div> | 37 | <div> |
67 | <label i18n>Features found on this instance</label> | 38 | <label i18n>Features found on this instance</label> |
diff --git a/client/src/app/signup/signup.component.scss b/client/src/app/signup/signup.component.scss index 90e1e8e74..6f61b78f7 100644 --- a/client/src/app/signup/signup.component.scss +++ b/client/src/app/signup/signup.component.scss | |||
@@ -1,16 +1,32 @@ | |||
1 | @import '_variables'; | 1 | @import '_variables'; |
2 | @import '_mixins'; | 2 | @import '_mixins'; |
3 | 3 | ||
4 | .alert { | ||
5 | font-size: 15px; | ||
6 | text-align: center; | ||
7 | } | ||
8 | |||
9 | .wrapper { | ||
10 | display: flex; | ||
11 | justify-content: space-between; | ||
12 | flex-wrap: wrap; | ||
13 | |||
14 | & > div { | ||
15 | margin-bottom: 40px; | ||
16 | width: 450px; | ||
17 | |||
18 | @media screen and (max-width: 500px) { | ||
19 | width: auto; | ||
20 | } | ||
21 | } | ||
22 | } | ||
23 | |||
4 | my-instance-features-table { | 24 | my-instance-features-table { |
5 | display: block; | 25 | display: block; |
6 | 26 | ||
7 | margin-bottom: 40px; | 27 | margin-bottom: 40px; |
8 | } | 28 | } |
9 | 29 | ||
10 | form { | ||
11 | margin: 0 60px 40px 0; | ||
12 | } | ||
13 | |||
14 | .form-group-terms { | 30 | .form-group-terms { |
15 | margin: 30px 0; | 31 | margin: 30px 0; |
16 | } | 32 | } |
@@ -25,15 +41,18 @@ form { | |||
25 | 41 | ||
26 | input:not([type=submit]) { | 42 | input:not([type=submit]) { |
27 | @include peertube-input-text(400px); | 43 | @include peertube-input-text(400px); |
44 | |||
28 | display: block; | 45 | display: block; |
29 | 46 | ||
30 | &#username { | 47 | &#username, |
31 | width: auto; | 48 | &#name { |
49 | width: auto !important; | ||
32 | flex-grow: 1; | 50 | flex-grow: 1; |
33 | } | 51 | } |
34 | } | 52 | } |
35 | 53 | ||
36 | input[type=submit] { | 54 | input[type=submit], |
55 | button { | ||
37 | @include peertube-button; | 56 | @include peertube-button; |
38 | @include orange-button; | 57 | @include orange-button; |
39 | } | 58 | } |
diff --git a/client/src/app/signup/signup.component.ts b/client/src/app/signup/signup.component.ts index 13941ec79..11eaa8521 100644 --- a/client/src/app/signup/signup.component.ts +++ b/client/src/app/signup/signup.component.ts | |||
@@ -1,22 +1,25 @@ | |||
1 | import { Component, OnInit } from '@angular/core' | 1 | import { Component } from '@angular/core' |
2 | import { AuthService, Notifier, RedirectService, ServerService } from '@app/core' | 2 | import { AuthService, Notifier, RedirectService, ServerService } from '@app/core' |
3 | import { UserCreate } from '../../../../shared' | 3 | import { UserService, UserValidatorsService } from '../shared' |
4 | import { FormReactive, UserService, UserValidatorsService } from '../shared' | ||
5 | import { I18n } from '@ngx-translate/i18n-polyfill' | 4 | import { I18n } from '@ngx-translate/i18n-polyfill' |
6 | import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service' | 5 | import { UserRegister } from '@shared/models/users/user-register.model' |
6 | import { FormGroup } from '@angular/forms' | ||
7 | 7 | ||
8 | @Component({ | 8 | @Component({ |
9 | selector: 'my-signup', | 9 | selector: 'my-signup', |
10 | templateUrl: './signup.component.html', | 10 | templateUrl: './signup.component.html', |
11 | styleUrls: [ './signup.component.scss' ] | 11 | styleUrls: [ './signup.component.scss' ] |
12 | }) | 12 | }) |
13 | export class SignupComponent extends FormReactive implements OnInit { | 13 | export class SignupComponent { |
14 | info: string = null | 14 | info: string = null |
15 | error: string = null | 15 | error: string = null |
16 | success: string = null | ||
16 | signupDone = false | 17 | signupDone = false |
17 | 18 | ||
19 | formStepUser: FormGroup | ||
20 | formStepChannel: FormGroup | ||
21 | |||
18 | constructor ( | 22 | constructor ( |
19 | protected formValidatorService: FormValidatorService, | ||
20 | private authService: AuthService, | 23 | private authService: AuthService, |
21 | private userValidatorsService: UserValidatorsService, | 24 | private userValidatorsService: UserValidatorsService, |
22 | private notifier: Notifier, | 25 | private notifier: Notifier, |
@@ -25,47 +28,55 @@ export class SignupComponent extends FormReactive implements OnInit { | |||
25 | private redirectService: RedirectService, | 28 | private redirectService: RedirectService, |
26 | private i18n: I18n | 29 | private i18n: I18n |
27 | ) { | 30 | ) { |
28 | super() | ||
29 | } | ||
30 | |||
31 | get instanceHost () { | ||
32 | return window.location.host | ||
33 | } | 31 | } |
34 | 32 | ||
35 | get requiresEmailVerification () { | 33 | get requiresEmailVerification () { |
36 | return this.serverService.getConfig().signup.requiresEmailVerification | 34 | return this.serverService.getConfig().signup.requiresEmailVerification |
37 | } | 35 | } |
38 | 36 | ||
39 | ngOnInit () { | 37 | hasSameChannelAndAccountNames () { |
40 | this.buildForm({ | 38 | return this.getUsername() === this.getChannelName() |
41 | username: this.userValidatorsService.USER_USERNAME, | 39 | } |
42 | password: this.userValidatorsService.USER_PASSWORD, | 40 | |
43 | email: this.userValidatorsService.USER_EMAIL, | 41 | getUsername () { |
44 | terms: this.userValidatorsService.USER_TERMS | 42 | if (!this.formStepUser) return undefined |
45 | }) | 43 | |
44 | return this.formStepUser.value['username'] | ||
45 | } | ||
46 | |||
47 | getChannelName () { | ||
48 | if (!this.formStepChannel) return undefined | ||
49 | |||
50 | return this.formStepChannel.value['name'] | ||
51 | } | ||
52 | |||
53 | onUserFormBuilt (form: FormGroup) { | ||
54 | this.formStepUser = form | ||
55 | } | ||
56 | |||
57 | onChannelFormBuilt (form: FormGroup) { | ||
58 | this.formStepChannel = form | ||
46 | } | 59 | } |
47 | 60 | ||
48 | signup () { | 61 | signup () { |
49 | this.error = null | 62 | this.error = null |
50 | 63 | ||
51 | const userCreate: UserCreate = this.form.value | 64 | const body: UserRegister = Object.assign(this.formStepUser.value, this.formStepChannel.value) |
52 | 65 | ||
53 | this.userService.signup(userCreate).subscribe( | 66 | this.userService.signup(body).subscribe( |
54 | () => { | 67 | () => { |
55 | this.signupDone = true | 68 | this.signupDone = true |
56 | 69 | ||
57 | if (this.requiresEmailVerification) { | 70 | if (this.requiresEmailVerification) { |
58 | this.info = this.i18n('Welcome! Now please check your emails to verify your account and complete signup.') | 71 | this.info = this.i18n('Now please check your emails to verify your account and complete signup.') |
59 | return | 72 | return |
60 | } | 73 | } |
61 | 74 | ||
62 | // Auto login | 75 | // Auto login |
63 | this.authService.login(userCreate.username, userCreate.password) | 76 | this.authService.login(body.username, body.password) |
64 | .subscribe( | 77 | .subscribe( |
65 | () => { | 78 | () => { |
66 | this.notifier.success(this.i18n('You are now logged in as {{username}}!', { username: userCreate.username })) | 79 | this.success = this.i18n('You are now logged in as {{username}}!', { username: body.username }) |
67 | |||
68 | this.redirectService.redirectToHomepage() | ||
69 | }, | 80 | }, |
70 | 81 | ||
71 | err => this.error = err.message | 82 | err => this.error = err.message |
diff --git a/client/src/app/signup/signup.module.ts b/client/src/app/signup/signup.module.ts index 61560ddcf..fccaf7ce1 100644 --- a/client/src/app/signup/signup.module.ts +++ b/client/src/app/signup/signup.module.ts | |||
@@ -1,17 +1,26 @@ | |||
1 | import { NgModule } from '@angular/core' | 1 | import { NgModule } from '@angular/core' |
2 | |||
3 | import { SignupRoutingModule } from './signup-routing.module' | 2 | import { SignupRoutingModule } from './signup-routing.module' |
4 | import { SignupComponent } from './signup.component' | 3 | import { SignupComponent } from './signup.component' |
5 | import { SharedModule } from '../shared' | 4 | import { SharedModule } from '../shared' |
5 | import { CdkStepperModule } from '@angular/cdk/stepper' | ||
6 | import { SignupStepChannelComponent } from '@app/signup/signup-step-channel.component' | ||
7 | import { SignupStepUserComponent } from '@app/signup/signup-step-user.component' | ||
8 | import { CustomStepperComponent } from '@app/signup/custom-stepper.component' | ||
9 | import { SuccessComponent } from '@app/signup/success.component' | ||
6 | 10 | ||
7 | @NgModule({ | 11 | @NgModule({ |
8 | imports: [ | 12 | imports: [ |
9 | SignupRoutingModule, | 13 | SignupRoutingModule, |
10 | SharedModule | 14 | SharedModule, |
15 | CdkStepperModule | ||
11 | ], | 16 | ], |
12 | 17 | ||
13 | declarations: [ | 18 | declarations: [ |
14 | SignupComponent | 19 | SignupComponent, |
20 | CustomStepperComponent, | ||
21 | SuccessComponent, | ||
22 | SignupStepChannelComponent, | ||
23 | SignupStepUserComponent | ||
15 | ], | 24 | ], |
16 | 25 | ||
17 | exports: [ | 26 | exports: [ |
diff --git a/client/src/app/signup/success.component.html b/client/src/app/signup/success.component.html new file mode 100644 index 000000000..68eb72b61 --- /dev/null +++ b/client/src/app/signup/success.component.html | |||
@@ -0,0 +1,8 @@ | |||
1 | <!-- Thanks: Amit Singh Sansoya from https://codepen.io/amit3200/pen/zWMJOO --> | ||
2 | |||
3 | <svg version="1.1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 130.2 130.2"> | ||
4 | <circle class="path circle" fill="none" stroke="#73AF55" stroke-width="6" stroke-miterlimit="10" cx="65.1" cy="65.1" r="62.1"/> | ||
5 | <polyline class="path check" fill="none" stroke="#73AF55" stroke-width="6" stroke-linecap="round" stroke-miterlimit="10" points="100.2,40.2 51.5,88.8 29.8,67.5 "/> | ||
6 | </svg> | ||
7 | |||
8 | <p class="success">Welcome on PeerTube!</p> | ||
diff --git a/client/src/app/signup/success.component.scss b/client/src/app/signup/success.component.scss new file mode 100644 index 000000000..7c66e08cf --- /dev/null +++ b/client/src/app/signup/success.component.scss | |||
@@ -0,0 +1,74 @@ | |||
1 | svg { | ||
2 | width: 100px; | ||
3 | display: block; | ||
4 | margin: 40px auto 0; | ||
5 | } | ||
6 | |||
7 | .path { | ||
8 | stroke-dasharray: 1000; | ||
9 | stroke-dashoffset: 0; | ||
10 | |||
11 | &.circle { | ||
12 | -webkit-animation: dash .9s ease-in-out; | ||
13 | animation: dash .9s ease-in-out; | ||
14 | } | ||
15 | |||
16 | &.line { | ||
17 | stroke-dashoffset: 1000; | ||
18 | -webkit-animation: dash .9s .35s ease-in-out forwards; | ||
19 | animation: dash .9s .35s ease-in-out forwards; | ||
20 | } | ||
21 | |||
22 | &.check { | ||
23 | stroke-dashoffset: -100; | ||
24 | -webkit-animation: dash-check .9s .35s ease-in-out forwards; | ||
25 | animation: dash-check .9s .35s ease-in-out forwards; | ||
26 | } | ||
27 | } | ||
28 | |||
29 | p { | ||
30 | text-align: center; | ||
31 | margin: 20px 0 60px; | ||
32 | font-size: 1.25em; | ||
33 | |||
34 | &.success { | ||
35 | color: #73AF55; | ||
36 | } | ||
37 | } | ||
38 | |||
39 | |||
40 | @-webkit-keyframes dash { | ||
41 | 0% { | ||
42 | stroke-dashoffset: 1000; | ||
43 | } | ||
44 | 100% { | ||
45 | stroke-dashoffset: 0; | ||
46 | } | ||
47 | } | ||
48 | |||
49 | @keyframes dash { | ||
50 | 0% { | ||
51 | stroke-dashoffset: 1000; | ||
52 | } | ||
53 | 100% { | ||
54 | stroke-dashoffset: 0; | ||
55 | } | ||
56 | } | ||
57 | |||
58 | @-webkit-keyframes dash-check { | ||
59 | 0% { | ||
60 | stroke-dashoffset: -100; | ||
61 | } | ||
62 | 100% { | ||
63 | stroke-dashoffset: 900; | ||
64 | } | ||
65 | } | ||
66 | |||
67 | @keyframes dash-check { | ||
68 | 0% { | ||
69 | stroke-dashoffset: -100; | ||
70 | } | ||
71 | 100% { | ||
72 | stroke-dashoffset: 900; | ||
73 | } | ||
74 | } | ||
diff --git a/client/src/app/signup/success.component.ts b/client/src/app/signup/success.component.ts new file mode 100644 index 000000000..2674e1e30 --- /dev/null +++ b/client/src/app/signup/success.component.ts | |||
@@ -0,0 +1,10 @@ | |||
1 | import { Component } from '@angular/core' | ||
2 | |||
3 | @Component({ | ||
4 | selector: 'my-success', | ||
5 | templateUrl: './success.component.html', | ||
6 | styleUrls: [ './success.component.scss' ] | ||
7 | }) | ||
8 | export class SuccessComponent { | ||
9 | |||
10 | } | ||
diff --git a/client/src/sass/include/_mixins.scss b/client/src/sass/include/_mixins.scss index 262a8136f..228a6116e 100644 --- a/client/src/sass/include/_mixins.scss +++ b/client/src/sass/include/_mixins.scss | |||
@@ -331,7 +331,12 @@ | |||
331 | } | 331 | } |
332 | 332 | ||
333 | @mixin peertube-checkbox ($border-width) { | 333 | @mixin peertube-checkbox ($border-width) { |
334 | display: none; | 334 | opacity: 0; |
335 | width: 0; | ||
336 | |||
337 | &:focus + span { | ||
338 | outline: auto; | ||
339 | } | ||
335 | 340 | ||
336 | & + span { | 341 | & + span { |
337 | position: relative; | 342 | position: relative; |
diff --git a/server/middlewares/validators/users.ts b/server/middlewares/validators/users.ts index b58dcc0d6..7a081af33 100644 --- a/server/middlewares/validators/users.ts +++ b/server/middlewares/validators/users.ts | |||
@@ -70,6 +70,12 @@ const usersRegisterValidator = [ | |||
70 | .end() | 70 | .end() |
71 | } | 71 | } |
72 | 72 | ||
73 | if (body.channel.name === body.username) { | ||
74 | return res.status(400) | ||
75 | .send({ error: 'Channel name cannot be the same than user username.' }) | ||
76 | .end() | ||
77 | } | ||
78 | |||
73 | const existing = await ActorModel.loadLocalByName(body.channel.name) | 79 | const existing = await ActorModel.loadLocalByName(body.channel.name) |
74 | if (existing) { | 80 | if (existing) { |
75 | return res.status(409) | 81 | return res.status(409) |
diff --git a/server/tests/api/check-params/users.ts b/server/tests/api/check-params/users.ts index d26032ea5..95097817b 100644 --- a/server/tests/api/check-params/users.ts +++ b/server/tests/api/check-params/users.ts | |||
@@ -737,6 +737,13 @@ describe('Test users API validators', function () { | |||
737 | await makePostBodyRequest({ url: server.url, path: registrationPath, token: server.accessToken, fields }) | 737 | await makePostBodyRequest({ url: server.url, path: registrationPath, token: server.accessToken, fields }) |
738 | }) | 738 | }) |
739 | 739 | ||
740 | it('Should fail with a channel name that is the same than user username', async function () { | ||
741 | const source = { username: 'super_user', channel: { name: 'super_user', displayName: 'display name' } } | ||
742 | const fields = immutableAssign(baseCorrectParams, source) | ||
743 | |||
744 | await makePostBodyRequest({ url: server.url, path: registrationPath, token: server.accessToken, fields }) | ||
745 | }) | ||
746 | |||
740 | it('Should fail with an existing channel', async function () { | 747 | it('Should fail with an existing channel', async function () { |
741 | const videoChannelAttributesArg = { name: 'existing_channel', displayName: 'hello', description: 'super description' } | 748 | const videoChannelAttributesArg = { name: 'existing_channel', displayName: 'hello', description: 'super description' } |
742 | await addVideoChannel(server.url, server.accessToken, videoChannelAttributesArg) | 749 | await addVideoChannel(server.url, server.accessToken, videoChannelAttributesArg) |