From 1d5342abc43df02cf0bd69b1e865c0f179182eef Mon Sep 17 00:00:00 2001 From: Chocobozzz Date: Wed, 29 May 2019 11:03:01 +0200 Subject: [PATCH] Multi step registration --- .../forms/peertube-checkbox.component.scss | 3 - client/src/app/shared/users/user.service.ts | 3 +- .../app/signup/custom-stepper.component.html | 25 ++++++ .../app/signup/custom-stepper.component.scss | 66 ++++++++++++++++ .../app/signup/custom-stepper.component.ts | 19 +++++ .../signup/signup-step-channel.component.html | 50 ++++++++++++ .../signup/signup-step-channel.component.ts | 40 ++++++++++ .../signup/signup-step-user.component.html | 54 +++++++++++++ .../app/signup/signup-step-user.component.ts | 37 +++++++++ client/src/app/signup/signup.component.html | 77 ++++++------------- client/src/app/signup/signup.component.scss | 33 ++++++-- client/src/app/signup/signup.component.ts | 61 +++++++++------ client/src/app/signup/signup.module.ts | 15 +++- client/src/app/signup/success.component.html | 8 ++ client/src/app/signup/success.component.scss | 74 ++++++++++++++++++ client/src/app/signup/success.component.ts | 10 +++ client/src/sass/include/_mixins.scss | 7 +- server/middlewares/validators/users.ts | 6 ++ server/tests/api/check-params/users.ts | 7 ++ 19 files changed, 502 insertions(+), 93 deletions(-) create mode 100644 client/src/app/signup/custom-stepper.component.html create mode 100644 client/src/app/signup/custom-stepper.component.scss create mode 100644 client/src/app/signup/custom-stepper.component.ts create mode 100644 client/src/app/signup/signup-step-channel.component.html create mode 100644 client/src/app/signup/signup-step-channel.component.ts create mode 100644 client/src/app/signup/signup-step-user.component.html create mode 100644 client/src/app/signup/signup-step-user.component.ts create mode 100644 client/src/app/signup/success.component.html create mode 100644 client/src/app/signup/success.component.scss create mode 100644 client/src/app/signup/success.component.ts 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 @@ input { @include peertube-checkbox(1px); - - width: 10px; - margin-right: 10px; } } 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' import { SortMeta } from 'primeng/api' import { BytesPipe } from 'ngx-pipes' import { I18n } from '@ngx-translate/i18n-polyfill' +import { UserRegister } from '@shared/models/users/user-register.model' @Injectable() export class UserService { @@ -64,7 +65,7 @@ export class UserService { .pipe(catchError(err => this.restExtractor.handleError(err))) } - signup (userCreate: UserCreate) { + signup (userCreate: UserRegister) { return this.authHttp.post(UserService.BASE_USERS_URL + 'register', userCreate) .pipe( 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 @@ +
+
+ +
+
+ {{ i + 1 }} + +
+ +
{{ step.label }}
+
+ + +
+
+
+ +
+ +
+ +
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 @@ +@import '_variables'; +@import '_mixins'; + +$grey-color: #9CA3AB; +$index-block-height: 32px; + +header { + display: flex; + justify-content: space-between; + font-size: 15px; + margin-bottom: 30px; + + .step-info { + color: $grey-color; + display: flex; + flex-direction: column; + align-items: center; + width: $index-block-height; + + .step-index { + display: flex; + justify-content: center; + align-items: center; + width: $index-block-height; + height: $index-block-height; + border-radius: 100px; + border: 2px solid $grey-color; + margin-bottom: 10px; + + my-global-icon { + @include apply-svg-color(var(--mainBackgroundColor)); + + width: 22px; + height: 22px; + } + } + + .step-label { + width: max-content; + } + + &.active, + &.completed { + .step-index { + border-color: var(--mainColor); + background-color: var(--mainColor); + color: var(--mainBackgroundColor); + } + + .step-label { + color: var(--mainColor); + } + } + + &.completed { + cursor: pointer; + } + } + + .connector { + flex: auto; + margin: $index-block-height/2 10px 0 10px; + height: 2px; + background-color: $grey-color; + } +} 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 @@ +import { Component } from '@angular/core' +import { CdkStep, CdkStepper } from '@angular/cdk/stepper' + +@Component({ + selector: 'my-custom-stepper', + templateUrl: './custom-stepper.component.html', + styleUrls: [ './custom-stepper.component.scss' ], + providers: [ { provide: CdkStepper, useExisting: CustomStepperComponent } ] +}) +export class CustomStepperComponent extends CdkStepper { + + onClick (index: number): void { + this.selectedIndex = index + } + + isCompleted (step: CdkStep) { + return step.stepControl && step.stepControl.dirty && step.stepControl.valid + } +} 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 @@ +
+ +
+

+ A channel is an entity in which you upload your videos. Creating several of them helps you to organize and separate your content.
+ 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. +

+ +

+ Other users can decide to subscribe any channel they want, to be notified when you publish a new video. +

+
+ +
+ + +
+ +
+ @{{ instanceHost }} +
+
+ +
+ {{ formErrors.name }} +
+ +
+ Channel name cannot be the same than your account name. You can click on the first step to update your account name. +
+
+ +
+ + +
+ +
+ +
+ {{ formErrors.displayName }} +
+
+
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 @@ +import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core' +import { AuthService } from '@app/core' +import { FormReactive, VideoChannelValidatorsService } from '../shared' +import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service' +import { FormGroup } from '@angular/forms' + +@Component({ + selector: 'my-signup-step-channel', + templateUrl: './signup-step-channel.component.html', + styleUrls: [ './signup.component.scss' ] +}) +export class SignupStepChannelComponent extends FormReactive implements OnInit { + @Input() username: string + @Output() formBuilt = new EventEmitter() + + constructor ( + protected formValidatorService: FormValidatorService, + private authService: AuthService, + private videoChannelValidatorsService: VideoChannelValidatorsService + ) { + super() + } + + get instanceHost () { + return window.location.host + } + + isSameThanUsername () { + return this.username && this.username === this.form.value['name'] + } + + ngOnInit () { + this.buildForm({ + name: this.videoChannelValidatorsService.VIDEO_CHANNEL_NAME, + displayName: this.videoChannelValidatorsService.VIDEO_CHANNEL_DISPLAY_NAME + }) + + setTimeout(() => this.formBuilt.emit(this.form)) + } +} 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 @@ +
+ +
+ + +
+ +
+ @{{ instanceHost }} +
+
+ +
+ {{ formErrors.username }} +
+
+ +
+ + +
+ {{ formErrors.email }} +
+
+ +
+ + +
+ {{ formErrors.password }} +
+
+ +
+ + +
+ {{ formErrors.terms }} +
+
+
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 @@ +import { Component, EventEmitter, OnInit, Output } from '@angular/core' +import { AuthService } from '@app/core' +import { FormReactive, UserValidatorsService } from '../shared' +import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service' +import { FormGroup } from '@angular/forms' + +@Component({ + selector: 'my-signup-step-user', + templateUrl: './signup-step-user.component.html', + styleUrls: [ './signup.component.scss' ] +}) +export class SignupStepUserComponent extends FormReactive implements OnInit { + @Output() formBuilt = new EventEmitter() + + constructor ( + protected formValidatorService: FormValidatorService, + private authService: AuthService, + private userValidatorsService: UserValidatorsService + ) { + super() + } + + get instanceHost () { + return window.location.host + } + + ngOnInit () { + this.buildForm({ + username: this.userValidatorsService.USER_USERNAME, + password: this.userValidatorsService.USER_PASSWORD, + email: this.userValidatorsService.USER_EMAIL, + terms: this.userValidatorsService.USER_TERMS + }) + + setTimeout(() => this.formBuilt.emit(this.form)) + } +} 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 @@ Create an account + +
{{ info }}
-
{{ error }}
+
{{ success }}
-
-
-
- +
+
+ + + -
- Next + + + + + +
+ Create my account + +
-
- {{ formErrors.username }} -
-
- -
- - -
- {{ formErrors.email }} -
-
- -
- - -
- {{ formErrors.password }} -
-
- -
- - -
- {{ formErrors.terms }} -
-
- - - + +
{{ error }}
+
+ +
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 @@ @import '_variables'; @import '_mixins'; +.alert { + font-size: 15px; + text-align: center; +} + +.wrapper { + display: flex; + justify-content: space-between; + flex-wrap: wrap; + + & > div { + margin-bottom: 40px; + width: 450px; + + @media screen and (max-width: 500px) { + width: auto; + } + } +} + my-instance-features-table { display: block; margin-bottom: 40px; } -form { - margin: 0 60px 40px 0; -} - .form-group-terms { margin: 30px 0; } @@ -25,15 +41,18 @@ form { input:not([type=submit]) { @include peertube-input-text(400px); + display: block; - &#username { - width: auto; + &#username, + &#name { + width: auto !important; flex-grow: 1; } } -input[type=submit] { +input[type=submit], +button { @include peertube-button; @include orange-button; } 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 @@ -import { Component, OnInit } from '@angular/core' +import { Component } from '@angular/core' import { AuthService, Notifier, RedirectService, ServerService } from '@app/core' -import { UserCreate } from '../../../../shared' -import { FormReactive, UserService, UserValidatorsService } from '../shared' +import { UserService, UserValidatorsService } from '../shared' import { I18n } from '@ngx-translate/i18n-polyfill' -import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service' +import { UserRegister } from '@shared/models/users/user-register.model' +import { FormGroup } from '@angular/forms' @Component({ selector: 'my-signup', templateUrl: './signup.component.html', styleUrls: [ './signup.component.scss' ] }) -export class SignupComponent extends FormReactive implements OnInit { +export class SignupComponent { info: string = null error: string = null + success: string = null signupDone = false + formStepUser: FormGroup + formStepChannel: FormGroup + constructor ( - protected formValidatorService: FormValidatorService, private authService: AuthService, private userValidatorsService: UserValidatorsService, private notifier: Notifier, @@ -25,47 +28,55 @@ export class SignupComponent extends FormReactive implements OnInit { private redirectService: RedirectService, private i18n: I18n ) { - super() - } - - get instanceHost () { - return window.location.host } get requiresEmailVerification () { return this.serverService.getConfig().signup.requiresEmailVerification } - ngOnInit () { - this.buildForm({ - username: this.userValidatorsService.USER_USERNAME, - password: this.userValidatorsService.USER_PASSWORD, - email: this.userValidatorsService.USER_EMAIL, - terms: this.userValidatorsService.USER_TERMS - }) + hasSameChannelAndAccountNames () { + return this.getUsername() === this.getChannelName() + } + + getUsername () { + if (!this.formStepUser) return undefined + + return this.formStepUser.value['username'] + } + + getChannelName () { + if (!this.formStepChannel) return undefined + + return this.formStepChannel.value['name'] + } + + onUserFormBuilt (form: FormGroup) { + this.formStepUser = form + } + + onChannelFormBuilt (form: FormGroup) { + this.formStepChannel = form } signup () { this.error = null - const userCreate: UserCreate = this.form.value + const body: UserRegister = Object.assign(this.formStepUser.value, this.formStepChannel.value) - this.userService.signup(userCreate).subscribe( + this.userService.signup(body).subscribe( () => { this.signupDone = true if (this.requiresEmailVerification) { - this.info = this.i18n('Welcome! Now please check your emails to verify your account and complete signup.') + this.info = this.i18n('Now please check your emails to verify your account and complete signup.') return } // Auto login - this.authService.login(userCreate.username, userCreate.password) + this.authService.login(body.username, body.password) .subscribe( () => { - this.notifier.success(this.i18n('You are now logged in as {{username}}!', { username: userCreate.username })) - - this.redirectService.redirectToHomepage() + this.success = this.i18n('You are now logged in as {{username}}!', { username: body.username }) }, 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 @@ import { NgModule } from '@angular/core' - import { SignupRoutingModule } from './signup-routing.module' import { SignupComponent } from './signup.component' import { SharedModule } from '../shared' +import { CdkStepperModule } from '@angular/cdk/stepper' +import { SignupStepChannelComponent } from '@app/signup/signup-step-channel.component' +import { SignupStepUserComponent } from '@app/signup/signup-step-user.component' +import { CustomStepperComponent } from '@app/signup/custom-stepper.component' +import { SuccessComponent } from '@app/signup/success.component' @NgModule({ imports: [ SignupRoutingModule, - SharedModule + SharedModule, + CdkStepperModule ], declarations: [ - SignupComponent + SignupComponent, + CustomStepperComponent, + SuccessComponent, + SignupStepChannelComponent, + SignupStepUserComponent ], 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 @@ + + + + + + + +

Welcome on PeerTube!

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 @@ +svg { + width: 100px; + display: block; + margin: 40px auto 0; +} + +.path { + stroke-dasharray: 1000; + stroke-dashoffset: 0; + + &.circle { + -webkit-animation: dash .9s ease-in-out; + animation: dash .9s ease-in-out; + } + + &.line { + stroke-dashoffset: 1000; + -webkit-animation: dash .9s .35s ease-in-out forwards; + animation: dash .9s .35s ease-in-out forwards; + } + + &.check { + stroke-dashoffset: -100; + -webkit-animation: dash-check .9s .35s ease-in-out forwards; + animation: dash-check .9s .35s ease-in-out forwards; + } +} + +p { + text-align: center; + margin: 20px 0 60px; + font-size: 1.25em; + + &.success { + color: #73AF55; + } +} + + +@-webkit-keyframes dash { + 0% { + stroke-dashoffset: 1000; + } + 100% { + stroke-dashoffset: 0; + } +} + +@keyframes dash { + 0% { + stroke-dashoffset: 1000; + } + 100% { + stroke-dashoffset: 0; + } +} + +@-webkit-keyframes dash-check { + 0% { + stroke-dashoffset: -100; + } + 100% { + stroke-dashoffset: 900; + } +} + +@keyframes dash-check { + 0% { + stroke-dashoffset: -100; + } + 100% { + stroke-dashoffset: 900; + } +} 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 @@ +import { Component } from '@angular/core' + +@Component({ + selector: 'my-success', + templateUrl: './success.component.html', + styleUrls: [ './success.component.scss' ] +}) +export class SuccessComponent { + +} 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 @@ } @mixin peertube-checkbox ($border-width) { - display: none; + opacity: 0; + width: 0; + + &:focus + span { + outline: auto; + } & + span { 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 = [ .end() } + if (body.channel.name === body.username) { + return res.status(400) + .send({ error: 'Channel name cannot be the same than user username.' }) + .end() + } + const existing = await ActorModel.loadLocalByName(body.channel.name) if (existing) { 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 () { await makePostBodyRequest({ url: server.url, path: registrationPath, token: server.accessToken, fields }) }) + it('Should fail with a channel name that is the same than user username', async function () { + const source = { username: 'super_user', channel: { name: 'super_user', displayName: 'display name' } } + const fields = immutableAssign(baseCorrectParams, source) + + await makePostBodyRequest({ url: server.url, path: registrationPath, token: server.accessToken, fields }) + }) + it('Should fail with an existing channel', async function () { const videoChannelAttributesArg = { name: 'existing_channel', displayName: 'hello', description: 'super description' } await addVideoChannel(server.url, server.accessToken, videoChannelAttributesArg)